This is part 3 of a series of posts in which I attempt to reproduce the Zune Now Playing interface using the MVVM pattern within WPF. An introduction to the series and table of contents can be found in the first post of this series.
In the previous post, I quickly threw together an interface that combined a number of album cover views into a parent grid. The datacontext for each view was bound directly to an Album entity that I created in a parent viewmodel. While this was enough to render the interface I was after, it suffered from a few shortcomings:
- The cover views were directly bound to domain entities pulled out of an ObservableCollection by index, rather than viewmodels with bindable properties. This is bad design, and it bypasses the usefulness of ObservableCollection.
- I manually created the list of Albums in the MainPageViewModel. This should really be pulled out to a service so that I can fetch Albums from other sources and make them available to any viewmodels in my application that require them
- The covers were static – they did not change, and there was no accommodation for allowing them to update.
I need to clean this up a bit before I start worrying about the Zune layout. Let’s approach these concerns one at a time.
Binding cover views to viewmodels
To bring this application more in line with the MVVM approach, I want the cover views to use a CoverViewModel as their datacontext. My first step is to create a CoverViewModel class that exposes some of the Album attributes.
public class CoverViewModel : ViewModelBase {
private Album theAlbum;
public string ArtworkUrl { get { return Album.ArtworkUrl; } }
public string Name { get { return Album.Name; } }
public Album Album { get { return theAlbum; } set {
theAlbum = value;
NotifyPropertyChanged("ArtworkUrl");
NotifyPropertyChanged("Name");
} }
}
And now I can use a CoverViewModel in my cover views. At first glance, I figured this would be easy – just add a resource in each of the three cover views, and set the datacontext of the container grid to bind to the resource. You know, something like this:
<UserControl.Resources>
<ViewModel:CoverViewModel x:Key="CVM" />
</UserControl.Resources>
<Grid DataContext="{Binding Source={StaticResource CVM}}" >
<Image Source="{Binding Path=ArtworkUrl}" />
<Label Content="{Binding Path=Name}" />
</Grid>
This would appear to be just what I’m after, but binding declaratively like this introduces another problem - how will I hand each individual instance of the Cover a unique Album? This design is incomplete because it only addresses the pull portion of the binding – it makes no accommodation for setting the Album in the CoverViewModel before it binds to the ArtworkUrl and Name properties. I need a way to package up an Album and a CoverViewModel and hand it off to a cover view when assembling my main window.
If I take a look at where I want to end up, I begin to see that there will need to be some master entity that maintains a list of bound CoverViewModels and available Albums. This entity will essentially do inventory management for the application – ensuring a high degree of ordered randomness in Album binding, and satisfying business rules around differing number of Albums and CoverViewModels. This leads me to my second point of consideration – how do I collect and store Album instances it in such a way that these binding and assembly issues will be resolved?
collection storage VERSUS CONSUMPTION
Now that I’ve introduced a CoverViewModel layer between the Album and the view, the view and Album should no longer be aware of each other. Instead, I’ll create a service in my application that is responsible for discovering a list of Albums and making them available to outside entities that wish to use them. The service will be a provider of Albums, and will work with some yet-unknown classes that will be consumers of Albums. I think I’ll break the two into separate interfaces – IAlbumConsumer and IAlbumProvider.public interface IAlbumProvider {
void Register(IAlbumConsumer anAlbumConsumer);
void RefreshAlbum(IAlbumConsumer anAlbumConsumer);
}
public interface IAlbumConsumer {
void UpdateAlbum(Album anAlbum);
}
I’ll move all of that manual Album creation code from the last post out of the MainWindowViewModel and into a separate class. The new StaticAlbumProvider will implement the IAlbumProvider interface, and will maintain a list of available albums, along with a list of registered IAlbumConsumers. A static singleton property gives us a single point of storage for Album data across multiple viewmodel instances. Relevant code follows:
internal class StaticAlbumProvider : IAlbumProvider {
private readonly Dictionary<Album, int> theAlbums = new Dictionary<Album, int>();
private readonly List<IAlbumConsumer> theViewModels = new List<IAlbumConsumer>();
internal static readonly IAlbumProvider Instance = new StaticAlbumProvider();
public StaticAlbumProvider() {
theAlbums.Add(new Album
{
Name = "Waking Up",
Artist = new Artist {Name = "OneRepublic"},
ArtworkUrl = "http://image.listen.com/something.jpg"
}, 0); // counts how many times cover has been used in UI
//add more albums here
}
public void Register(IAlbumConsumer aCoverViewModel) {
theViewModels.Add(aCoverViewModel);
aCoverViewModel.UpdateAlbum(GetNextRandomAlbum());
}
public void RefreshAlbum(IAlbumConsumer anAlbumConsumer) {
anAlbumConsumer.UpdateAlbum(GetNextRandomAlbum());
}
private Album GetNextRandomAlbum() {
//random Album retrieval from theAlbums
return anAlbum;
}
}
public class CoverViewModel : ViewModelBase, IAlbumConsumer {
private Album theAlbum;
public CoverViewModel() {
StaticAlbumProvider.Instance.Register(this);
}
public string ArtworkUrl { get { return theAlbum.ArtworkUrl; } }
public string Name { get { return theAlbum.Name; } }
public void UpdateAlbum(Album anAlbum) {
theAlbum = anAlbum;
NotifyPropertyChanged("ArtworkUrl");
NotifyPropertyChanged("Name");
}
}
bringing together our views and viewmodels
The next step is to figure out a way to hand views different instances of the viewmodels. In a future post I’ll talk about View Model Locators, but for now I’ll take the same shortcut that I took with the StaticAlbumProvider. A simple ViewModelLocator class…public class ViewModelLocator {
public static readonly ViewModelLocator Instance = new ViewModelLocator();
public IAlbumConsumer CoverViewModel { get { return new CoverViewModel(); } }
}
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:svc="clr-namespace:NowPlayingMVVM.Service">
<svc:ViewModelLocator x:Key="Locator" />
</ResourceDictionary>
<UserControl.Resources>
<ResourceDictionary Source="../Resources.xaml" />
</UserControl.Resources>
<Grid DataContext="{Binding Source={StaticResource Locator}, Path=CoverViewModel}">
<Image Source="{Binding Path=ArtworkUrl}" />
<Label Content="{Binding Path=Name}" />
</Grid>
Rotating the collection
My last challenge for now is to tackle the problem that the album cover art is static and doesn’t rotate like the Zune Now Playing interface does. The way I constructed my IAlbumConsumer and IAlbumProvider interfaces allows me to force the update from either side of their relationship. It seems to make the most sense to me to drive the updates from the StaticAlbumProvider class, since this is where I’ll eventually keep track of which albums have been bound to the UI, and how many instances of that Album are currently showing.
private void RefreshAnAlbum() {
theRefreshTimer = new Timer {
Interval = (new Random().Next(5) + 1) * 1000,
AutoReset = true
};
theRefreshTimer.Elapsed += delegate {
theViewModels[new Random().Next(theViewModels.Count)].UpdateAlbum(GetNextRandomAlbum());
};
theRefreshTimer.Start();
}
With this change, the covers rotate every few seconds, even on the design surface.
Ok, so at this point I feel like I’ve got enough code in place to begin attacking the layout issues of my original goal – duplicating the Zune Now Playing interface. After I have that worked out, I’ll return to this design and look at introducing some existing frameworks to replace my custom viewmodel-related code.
No comments:
Post a Comment