Thursday, November 25, 2010

WPF – Declaratively binding a ViewModel

The MVVM pattern is widely adopted within WPF and Silverlight. The very flexible binding mechanism introduced with WPF allows for a loose coupling between the view and its model. The only thing you have to do is to set the view model as DataContext for the view. All further communication can then be done by binding the properties of the model to the view. Frameworks like Prism attempt to decouple the view model as far as possible from the view so that there are no unnecessary dependencies. But even in the best case the view has to know the type of the model in order to get it as its DataContext:

<UserControl.DataContext>
  <mv:ViewModel />
</UserControl.DataContext>

I’ll show you in this blog post how to achieve even more decoupling by declaratively specifying the view model using attributes. The concept depends on an attribute which marks a view model and a container that contains references to all found view model types within an assembly. So first we define the following attribute to mark all our view models:

[AttributeUsage(AttributeTargets.Class)]
public class DataContextAttribute : Attribute
{
    public DataContextAttribute(string key)
    {
        Key = key;
    }

    public string Key
    {
        get;       
    }
}

The attribute can be applied to any class and needs a unique key so that it can be identified later on. We can now use this new attribute to decorate our view model class. For the key I chose to use a GUID. You may use any other string value as well:

[DataContext("1C052C7C-E346-4CD9-A20C-D0617FA6F73D")]
public class WpfUserControlViewModel
{
   
}

That was pretty easy so far. Now we need a component that searches all types in our application for the data context attribute and stores that information accordingly. We do this by using reflection to look for the attribute. A separate class will store the found information in a dictionary:

public static class ViewModelContainerReference
{
    private static readonly ViewModelContainer Container = new ViewModelContainer();

    public static object Items
    {
        get { return Container; }
    }

    public static void Initialize()
    {
        Type[] types = Assembly.GetEntryAssembly().GetTypes();
        foreach (var type in types)
        {
            object[] attr = type.GetCustomAttributes(
  typeof(Utilities.DataContextAttribute), false);
            if (attr.Length == 1)
            {
                Container.Add(
                  ((DataContextAttribute)attr[0]).Key, type);
            }
        }
    }
}

The container which is of type ViewModelContainer stores the key and the according type in a dictionary. More precisely the container inherits the generic dictionary class and has only one indexer method that returns a new instance of a type to a given key:

public class ViewModelContainer : Dictionary<string, Type>
{
    public new object this[string name]
    {
        get
        {
            Type type;
            if (TryGetValue(name, out type))
            {
                return Activator.CreateInstance(type);
            }
            return null;
        }

    }
}

So examining the code you’ll probably notice that we expect every view model to have a parameterless constructor in order to create it vith the Activator. Another condition that arises from the whole concept is that the initialization code runs before any view which uses the concept is displayed. Otherwise no according view model will be found.
With the ViewModelContainerReference and the ViewModelContainer we can now specify the data context for a view like so:

DataContext="{Binding Source={x:Static Utilities:ViewModelContainerReference.Items}, Path=[1C052C7C-E346-4CD9-A20C-D0617FA6F73D]}"

The concrete view model is now given by a string in the Path of the Binding property. The string corresponds to the key defined in the data context attribute. When the view gets initialized it accesses the Items collection of the ViewModelContainerReference and passes the string in the Path property as parameter. Therefore the view no longer needs a reference to the type of the model. So now, you have to possibility to change the view model type without changing the view as long as you provide the correct key.

No comments:

Post a Comment