13 August, 2010

Now Playing with MVVM – Separating View Models and Services

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);
}
IAlbumConsumers will register with IAlbumProviders, who will provide them with Albums when requested. I’ve added a method for IAlbumProviders to push an update to individual IAlbumConsumers, as well as a way for an IAlbumConsumer to call in to to an IAlbumProvider and ask for a refresh.
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;
}
}
Implementing the IAlbumConsumer interface on CoverViewModel is straight forward. Note how a CoverViewModel registers itself upon creation with the StaticAlbumProvider instance. When it does so, the StaticAlbumProvider calls back into the CoverViewModel UpdateAlbum method, handing the viewmodel the album it needs:

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(); } }
}
…allows me to put a keyed resource in a project-wide resource dictionary…

<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>
…and bind to that resource by key name inside my CoverViewModels…

<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>
…providing context for the binding directives inside the viewmodel. Using a static resource retains the blendability of the cover viewmodels, meaning the album art will render in design view within Visual Studio or Blend.
CoverViewArt

 


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.
CoverArrayWithArt
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.


Download Project

No comments:

Post a Comment