MVVM ViewModel Locator via WPF Converters

Laurent Bugnion’s work on MVVM Light introduced me to the ViewModel Locator pattern. The concept and implementation are really slick!

From my perspective, ViewModel Locators do the following:

  1. Provide a simple, declarative way to connect a View to a ViewModel (duh).
  2. Enable design-time support, including the ability to connect an alternate data source behind the VM (cool!).

I want a solution that lets me:

  1. Connect my XAML views to their associated ViewModels
  2. Handle a somewhat complex model with a hierarchy of domain model entities and collections.
  3. Support “state” in the ViewModel and remember that state during the lifetime of the application.

After installing and attempting to use the MVVM Light implementation, I found that it wasn’t quite working the way I needed. After fumbling around a bit, doing some research, I stumbled on the idea of using a WPF converter (IValueConverter).

I’ve put together a sample application which demonstrates this pattern and shows some of the scenarios that would otherwise be difficult to achieve.

Here is a link to the source code for the demo:

Demo Application – MVVM ViewModel Locators via IValueConverter Converters

Here you can see the initial state of the application. It has some inline text and a walkthrough scenario so that you can see the behavior of the app.

image

Here you can see the app after walking though the usage steps. The inline text also highlights a couple key points to notice from the behavior.

image

Enough Chit-Chat. Code Please!

Let’s step back for just a moment and set some context. Laurent Bugnion’s VM Locators are design such that you create a special class in your application that can generate ViewModels as needed. This class is included as a resource in the app.xaml…

<vm:ViewModelLocatorTemplate 
   xmlns:vm="clr-namespace:ProjectForTemplate.ViewModel" 
   x:Key="Locator" 
/>        

… and then used in your code like this…

<DockPanel 
    DataContext="{Binding Source={StaticResource Locator}, Path=ViewModelName}"
    LastChildFill="True"
>

 

The class itself has properties for each of the ViewModels that you need to use in your application. The logic behind these includes some great features that create sample data for design time support.

However…

… I ran into some issues when I tried to use this with a domain model that included a hierarchy of inter-related objects and collections of objects. I was unable to see how I could use a single class, with static properties to return multiple instances of a single type of ViewModel.

I considered an approach that would pre-construct a hierarchy of ViewModels that matched the hierarchy of the domain model. Then I could simply pass that into my views and bind accordingly. However, at the suggestion of Richard Broida, I took the alternate approach of creating the ViewModels separately, on demand, and caching them. The end result feels right to me.

The XAML

Here is my code from my MainWindow of the sample app. As you can see, I have a hierarchy of domain model objects where a parent object might have multiple children in a collection.

<Window x:Class="ViewModelLocatorsWithConverters.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Converter ViewModel Locator Demo" 
        Height="600" Width="800" 
>

    <!-- DataContext: MainViewModel -->
    <DockPanel 
        DataContext="{Binding Converter={StaticResource MainVMLocator}}"
        LastChildFill="True"
    >
        <ScrollViewer VerticalScrollBarVisibility="Auto" DockPanel.Dock="Right">
             ...
        </ScrollViewer>
        <StackPanel DockPanel.Dock="Top">
            <DockPanel HorizontalAlignment="Stretch">

                <Label Margin="0" VerticalContentAlignment="Bottom" 
                       VerticalAlignment="Bottom" 
                       FontSize="16" 
                       FontWeight="Bold"
                       DockPanel.Dock="Left"
                >Families:</Label>


                <Button Command="{Binding LoadFamiliesCommand}" 
                        HorizontalAlignment="Right" 
                        Margin="5" MinWidth="100"
                        DockPanel.Dock="Right"
                >Load Families</Button>

                <Label Margin="0" VerticalContentAlignment="Bottom" HorizontalAlignment="Right"
                       VerticalAlignment="Bottom" FontSize="16" FontWeight="Bold" Foreground="Green"
                       DockPanel.Dock="Right"
                >Start Here -&gt;</Label>

            </DockPanel>
            <ComboBox 
                ItemsSource="{Binding Families}" 
                DisplayMemberPath="FamilyName" 
                IsSynchronizedWithCurrentItem="True"
                SelectedItem="{Binding CurrentFamily}" Margin="5">
            </ComboBox>
        </StackPanel>

        <!-- DataContext: FamilyViewModel -->
        <DockPanel 
            DataContext="{Binding CurrentFamily, Converter={StaticResource FamilyVMLocator}}" 
            Margin="0,10,0,0">

            <DockPanel DockPanel.Dock="Top" Margin="0">
                <Label VerticalAlignment="Bottom" VerticalContentAlignment="Bottom" 
                       FontSize="16" FontWeight="Bold">Family Members:</Label>
                <Button 
                    Command="{Binding TogglePetsCommand}" 
                    HorizontalAlignment="Right"
                    Content="{Binding ShowPetsButtonText}" Margin="5" MinWidth="100" />
            </DockPanel>

            <ListBox HorizontalContentAlignment="Stretch"
                ItemsSource="{Binding Family.Members}" 
                SelectedItem="{Binding SelectedPerson}" Margin="5">
                <ListBox.ItemTemplate>
                    <DataTemplate>

                        <!-- DataContext: PersonViewModel -->
                        <Grid 
                            HorizontalAlignment="Stretch"
                            DataContext="{Binding Converter={StaticResource PersonVMLocator}}" 
                        >
                            <Border BorderThickness="2" BorderBrush="Green" 
                                    CornerRadius="4" HorizontalAlignment="Stretch">
                                <TextBlock Margin="10, 2" MinWidth="200" FontFamily="Verdana">
                                    <TextBlock Text="{Binding FormattedName}" FontWeight="Bold" />
                                    <LineBreak/>
                                    <TextBlock Text="  (" />
                                    <TextBlock Text="{Binding Person.Role}" />
                                    <TextBlock Text=")" />
                                </TextBlock>
                            </Border>
                        </Grid>

                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </DockPanel>

    </DockPanel>
</Window>

 

I’ve highlighted the bindings to help pull out the data that is rendered and the interaction with the ViewModel.

Notice the three uses of a “Locator” such as:

DataContext =”{ Binding Converter ={ StaticResource MainVMLocator }}”

These are references to the “Converter-Based ViewModel Locators”. They are pulled into that application via declarations in the app.xaml file as follows:

<Application x:Class="ViewModelLocatorsWithConverters.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:l="clr-namespace:ViewModelLocatorsWithConverters.Locators"
             StartupUri="MainWindow.xaml">
    <Application.Resources>

        <!-- 
        ********************************************************* 
        Declare VM Locators here for use throughout the application. 
        *********************************************************  
        -->
        <l:FamilyViewModelLocator x:Key="FamilyVMLocator" />
        <l:MainViewModelLocator x:Key="MainVMLocator" />
        <l:PersonViewModelLocator x:Key="PersonVMLocator" />
        
    </Application.Resources>
</Application>

 

 

 

There is one locator created for each ViewModel in the application.

Converter-Based Locators

So what does this converter based locator look like? The following class is one of three in the sample application. It’s job is to do the best it can to get a reference to the DomainModel using whatever was passed in as a parameter to the converter. Once it has a reference to the DomainModel instance, it can see if it already has a cached ViewModel, or it can create a new instance of the ViewModel.

using System;
using System.Windows.Data;
using ViewModelLocatorsWithConverters.Model;
using ViewModelLocatorsWithConverters.ViewModels;
using System.Globalization;

namespace ViewModelLocatorsWithConverters.Locators
{
    /// <summary>
    /// Provides a method and a XAML IValueConverter to get the viewmodel 
    /// that is associated with the specified model or view model.
    /// </summary>
    public class PersonViewModelLocator : IValueConverter
    {

        /// <summary>
        /// For the specified parameter, find or create an appropriate viewmodel.
        /// </summary>
        /// <param name="value">An instance of a "parent" view model, 
        /// or an instance of the domain model class.</param>
        /// <returns>A new, or previously used view model instance.</returns>
        public static PersonViewModel GetPersonViewModel(object value)
        {
            // Check if we already have an instance of the viewmodel
            var vm = value as PersonViewModel;

            if (null == vm)
            {
                // The instance of the model that the ViewModel will handle.
                Person model = null;

                // See if the specified parameter is a known type.
                var Person = value as Person;

                // Pull the desired domain model class from the parameter.
                if (ViewModelLocator.IsInDesignMode)
                    model = ViewModelLocator.InitDesignTimeInstance().Members[0];
                else if (null != Person)
                    model = Person;
                else
                    throw new ArgumentException("Value parameter must be an instance of a " + 
                        "Person or PersonViewModel. Specified value is of type " + 
                        (null == value ? "<null>" : value.GetType().AssemblyQualifiedName), 
                        "value");

                // Get or create the ViewModel
                vm = ViewModelLocator.GetViewModel<PersonViewModel>(model);
                if (null == vm)
                {
                    vm = new PersonViewModel(model);

                    // Save the VM in the cache. It will be used again if the same Person is shown again.
                    ViewModelLocator.CacheViewModel(model, vm);
                }
            }

            return vm;
        }


        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return GetPersonViewModel(value);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

 

The ViewModel Cache

A single ViewModelLocator helper class stores the cache of ViewModels. This cache is keyed by the DomainModel instance. Here is the relevant code:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
using ViewModelLocatorsWithConverters.Model;
using ViewModelLocatorsWithConverters.ViewModels;

namespace ViewModelLocatorsWithConverters.Locators
{

    /// <summary>
    /// Stores a cache of view model instances. Also detects design time mode.
    /// </summary>
    /// <remarks>
    /// This is an internal class for use by the ViewModelLocator classes.
    /// Portions of this implementation are based on work by Laurent Bugnion (Galasoft).
    /// </remarks>
    public static class ViewModelLocator
    {

        #region Member Variables

        // Store a list of ViewModel instances, keyed by their associated model instance.
        // Key is a composite of the related instance plus the type of viewmodel. 
        // This is because a given instance could have multiple views.
        private static Dictionary<Tuple<object, Type>, ViewModelBase> _ViewModelCache 
            = new Dictionary<Tuple<object, Type>, ViewModelBase>();

        // Flag indicating whether the view is loaded in the designer.
        // Access via the property getter.
        private static bool? _isInDesignMode;

        #endregion

        #region Constructors

        #endregion

        #region Public Methods


        /// <summary>
        /// Gets a value indicating whether the control is in design mode
        /// (running in Blend or Visual Studio).
        /// </summary>
        public static bool IsInDesignMode
        {
            get
            {
                if (!_isInDesignMode.HasValue)
                {
#if SILVERLIGHT
                    _isInDesignMode = DesignerProperties.IsInDesignTool;
#else
                    var prop = DesignerProperties.IsInDesignModeProperty;
                    _isInDesignMode
                        = (bool)DependencyPropertyDescriptor
                        .FromProperty(prop, typeof(FrameworkElement))
                        .Metadata.DefaultValue;

                    // Just to be sure
                    if (!_isInDesignMode.Value
                        && Process.GetCurrentProcess().ProcessName.StartsWith("devenv", StringComparison.Ordinal))
                    {
                        _isInDesignMode = true;
                    }
#endif
                }

                return _isInDesignMode.Value;
            }
        }


        #endregion

        #region Internal Methods

        /// <summary>
        /// Get a previously cached ViewModel from the cache.
        /// </summary>
        /// <typeparam name="T">The type of the ViewModel instance.</typeparam>
        /// <param name="model">
        /// A reference to the instance of the model that is related to this ViewModel instance.
        /// This can be null if there is no model, as is often the case for the "main" or "root" ViewModel.
        /// </param>
        /// <returns>The cached ViewModel, or null if none is found.</returns>
        internal static T GetViewModel<T>(object model) where T : ViewModelBase
        {
            var key = new Tuple<object, Type>(model, typeof(T));
            if (_ViewModelCache.ContainsKey(key))
                return _ViewModelCache[key] as T;
            else
                return null;
        }

        /// <summary>
        /// Cache a ViewModel so that it can be used again if/when the domain model is
        /// displayed again.
        /// </summary>
        /// <param name="model">
        /// A reference to the instance of the model that is related to this ViewModel instance.
        /// This can be null if there is no model, as is often the case for the "main" or "root" ViewModel.
        /// </param>
        /// <param name="viewModel">
        /// The instance of the ViewModel to cache.
        /// </param>
        internal static void CacheViewModel(object model, ViewModelBase viewModel)
        {
            var key = new Tuple<object, Type>(model, viewModel.GetType());
            _ViewModelCache.Add(key, viewModel);
        }

        /// <summary>
        /// Remove a ViewModel from the cache.
        /// </summary>
        /// <typeparam name="T">The type of the ViewModel to clear.</typeparam>
        /// <param name="model">The instance of the model that this ViewModel was linked to. This can be null.</param>
        internal static void ClearViewModel<T>(object model) where T : ViewModelBase
        {
            var key = new Tuple<object, Type>(model, typeof(T));
            ViewModelBase vm = null;

            if (_ViewModelCache.ContainsKey(key))
                vm = _ViewModelCache[key];

            if (vm != null)
            {
                _ViewModelCache.Remove(key);
            }
        }

        #endregion

        #region Designer Data Helpers

        /// <summary>
        /// Create some instances of the domain model for use during design time.
        /// </summary>
        /// <returns></returns>
        internal static Family InitDesignTimeInstance()
        {
            Family family = new Family() { 
                FamilyName = "Design Time Family" 
            };

            family.Members.Add(new Person()
            {
                Name = "John Doe",
                Role = "Father"
            });

            family.Members.Add(new Person()
            {
                Name = "Jane Doe",
                Role = "Mother"
            });

            family.Members.Add(new Person()
            {
                Name = "Jimmy Doe",
                Role = "Child"
            });

            family.Members.Add(new Person()
            {
                Name = "Jill Doe",
                Role = "Child"
            });

            family.Members.Add(new Person()
            {
                Name = "Spot",
                Role = "Dog"
            });
            
            return family;
        }

        #endregion

    }
}

This class is based on the work of Laurent Bugnion in his MVVM Light implantation. I’ve added supporting methods for caching and retrieving ViewModel instances.

Benefits and Shortcomings

The above approach really shines in a couple scenarios:

Bound collections

If you have a collection of domain model objects that you want to bind into the view, you could simply bind that collection directly. That works for simple cases.

What if you have a need to do some formatting, add some commands, do some additional lookups, etc? That’s what ViewModels are for, right? But how do I inject a ViewModel between the collection and the ItemsControl / ListBox / etc.? You could do something like I’ve mentioned in a previous post using Bindable Linq, Continuous Linq, Obtics, or even the new Linq library from ComponentOne. That has it’s benefits, but might not be an option.

The MVVM Light locator doesn’t seem to help here because I only get a single instance of the ViewModel from the Locator, and it isn’t “hooked up” to the instance of the domain model that’s in my collection.

So…

Using a converter, I can “inject” a ViewModel during the binding operation such that the items in my ItemsControl / ListBox / whatever are now bound to an instance of a ViewModel.

On top of that, the ViewModel is “married” to the instance of the domain model so that I can have item-specific view related state. This state might include things like filter options, positions, sizes, item visibility, etc. This information might not have a durable storage location, but it’s not something that should be lost just because I went to another tab / page / item / view in my application.

Composable UIs

There are some cases, where you might create a user control that is shown from many locations in the application. This user control needs to get a reference to the ViewModel on which it depends. How does one do this?

You could set the DataContext in each location that you use the control, but…

A Converter-based locator provides some nice benefits in that the user control can figure it out for itself. The locator takes in the “ambient DataContext” based on where it is hosted in the UI. This ambient context could be another ViewModel, another DomainModel, the actual ViewModel used by the control, or the actual DomainModel used by the control.

In any of the above cases, the locator simply inspects the instance passed to the converter and reacts accordingly. In the case of a “parent” ViewModel, the converter might be able to use a property on that ViewModel to get a reference to the instance needed by the UserControl.

Design-Time Support

As with MVVM Light, the above implementation enables design-time support, making the solution “Blend-able”. You can see in the screen shot below that the drop down has some design time data in it. You can also see that the ListBox has items in it based on the data that I created in my ViewModelLocator when it detected design time.

Design time support in VS 2010

Shortcomings

There are some known issues in the current version. The main item is the fact that the cache holds a strong reference to the ViewModel and the DomainModel objects. This can be overcome with the use of WeakReferences and the WeakEventManager. I hope to address this soon.

Related to this is the question of “closing” or cleaning up an instance. This is something that I also hope to address soon. I haven’t had a strong need to dispose of ViewModels or DomainModels yet, but I know that it is an important scenario.

Call to Action

Please let me know what you think.

Maybe I missed some simple concept with MVVM Light that would make all of this unnecessary. Maybe there are other implementations that do this pattern already. Maybe you see a major flaw in the approach.

… Or maybe this is useful.

I’m hoping for useful ;-]

Cheers!

 

P.S. In case you missed it, the link to the source code for the demo is included at the top of this post.

One thought on “MVVM ViewModel Locator via WPF Converters

Add yours

  1. Thanks Leo!You raise a good point and one that I’ve considered a couple times. I don’t like the fact that the current version requires a separate locator class for each viewmodel. However, I’m not sure how to get around it cleanly. In simple cases, there are shortcuts that you can take. For example, if there is only ever on instance of a model or viewmodel, then you can shortcut some of the logic.The real benefit of this implementation is for cases where you have a collection of model instances and need to bind those to individual instances of corresponding viewmodels. In that case, the complexity is somewhat necessary.I’ve considered a base class, and that could make it simpler / cleaner. However, I’m not sure how to factor this down to just a single class without getting a really large class.One other thing I’ve considered is if there is a way to move some of the logic into some sort of metadata. Maybe the logic of mapping the ConverterParameter to the appropriate model could be defined as attributes on the ViewModel. Then a common ViewModelLocator could use the metadata to find/create the ViewModel.

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a website or blog at WordPress.com

Up ↑

%d bloggers like this: