Friday, April 23, 2010

Yet Another MVVM Locator Pattern

En español — Otro patrón de localización de modelos de vista

Been working with a lot of customers using the MVVM pattern with MEF and the issue always comes up regarding how to marry views to view models. There have a been a few musings on this, from my own ViewModel locator pattern to the even more advanced (and elegant) locator proposed by John Papa et al.

This is another pattern you may find useful. Most of the time your views are used in conjunction with some sort of navigation paradigm, whether it uses the navigation framework provided by Silverlight or a custom region management-type solution. The nice thing about using MEF for these needs is that the Lazy operator ensures the control is not wired up until it is needed. By some simple "magic" if we are using this pattern, we can easily bind the view to the view model in a totally decoupled manner.

First, we simply create a custom attribute for the view and the corresponding interface to make it easy to extract meta-data with MEF:

[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ViewOfTypeAttribute : ExportAttribute 
{     
    public string TypeName { get; set; }
    
    public ViewOfTypeAttribute(string typeName) : base(typeof(UserControl))
    {
        TypeName = typeName; 
    }
}

public interface IViewOfTypeCapabilities 
{
    string TypeName { get; }
}

Now we have a "magic string" we can tag the view with. If you like, you can convert it to an enum and have it typed, but this gives us more flexibility for other modules we may not know about. So my contact view might get tagged like this:

[ViewOfType("Contact")]
public partial class ContactView : UserControl
{
   public ContactView() 
   {
      InitializeComponent();
   }
}

Now we just make another tag for our view model. Almost all implementations of MVVM have some sort of base view model to help with things like firing the INotifyPropertyChanged event and so forth. Therefore, it's easy to target an attribute to the view model like this:

[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ViewModelOfTypeAttribute : ExportAttribute 
{     
    public string TypeName { get; set; }
    
    public ViewModelOfTypeAttribute(string typeName) : base(typeof(ViewModelBase))
    {
        TypeName = typeName; 
    }
}

public interface IViewModelOfTypeCapabilities 
{
    string TypeName { get; }
}

Look familiar? It should, because that's how we route views to view models. This way, even if multiple views use the same view model, we can still bind them together. When I make my view model, I simply tag it for export like this:

[ViewModelOfType("Contact")] 
public class ContactViewModel : ViewModelBase 
{
}

Notice the string I passed is the same for the view and the view model. These must match for the binding to occur! Now, I'm purposefully leaving out the navigation implementation. There are many ways this might happen, from swapping a ContentControl to using an ItemsControl and simply switching visibility. At some point, you will instantiate a copy of your view. When that happens, you can automatically bind the viewmodel if it is available. Here's a snippet that should give you the general idea:

[Export]
public class Binder  
{
   [ImportMany(AllowRecomposition=true)]
   public Lazy<UserControl,IViewOfTypeCapabilities> Views { get; set; }

   [ImportMany(AllowRecomposition=true)]
   public Lazy<ViewModelBase,IViewModelOfTypeCapabilities> ViewModels { get; set; }

   private _Bind(string binding) 
   {
      var viewModel = (from vm in ViewModels where vm.Metadata.TypeName.Equals(binding) select vm.Value).FirstOrDefault();
      
      foreach(var view in (from v in Views where v.Metadata.TypeName.Equals(binding) select v.Value).ToArray())
      {
         view.DataContext = viewModel;           
      }
   }
}

The _Bind command is simply called the first time the view is loaded onto a surface (added to a control, for example).

Now anytime you need to wire the two together, forget XAML or code behind or any of that nonsense. Just tag the view and the view model with a common attribute and they will get glued together for you!

Jeremy Likness