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

11 comments:

  1. Nice but if no use if you need non shared.

    ReplyDelete
  2. If I want nonshared, I'll use ExportFactory instead.

    ReplyDelete
  3. What calls _Bind? It's not clear to me how/where this fits into the composition process.

    ReplyDelete
  4. _Bind depends on your navigation. For example, if I am stacking views in an ItemsControl and simply activating them, then the first time I have a view, I add it to the ItemsControl children and then call _Bind in my navigation event. If I'm using a ContentControl and swapping in and out, then I'd call _Bind when swapping views the first time.

    ReplyDelete
    Replies
    1. could you show me some sample code for ItemsControl call _Bind. I try to couple ways but i seems does not work for me.

      Delete
    2. could you show me some sample code for itemcontrol call _bind function.

      Delete
  5. Not sure if I'm missing something.. but using the Binder class from your example, you would never be able to call _Bind() since it's marked private.

    ReplyDelete
  6. Take a look at my more recent post for an example. One class does the routing, so it internally calls the _Bind method. It's in the code example I just posted.

    ReplyDelete
  7. very cool, I would just use a System.Type instead of a string - I modified this in your new sample and it works perfectly.

    ReplyDelete
  8. You could use type, but then how would you hookup multiple views to the same viewmodel?

    ReplyDelete