5 Comments

Using a Design-Time ViewModelLocator With Caliburn.Micro

I was introduced to Caliburn.Micro less than a year ago, and it has since become my preferred MVVM framework for XAML development. It supports several conventions that reduce boilerplate code. It’s an opinionated framework that encourages a ViewModel-First approach during development.

Unfortunately, design-time support out of the box is limited to ViewModels with a public parameterless constructor1. This works for a while, but it becomes tedious to duplicate and maintain code that only exists for design-time support. I now use a DesignTimeViewModelLocator class for binding sample data in the designer.

Here’s how it works:

The XAML Side


d:DataContext="{Binding Source={d:DesignInstance GlobalFiltersViewModel},
                Converter={x:Static Global:DesignTimeViewModelLocator.Instance}}"
  • d:DataContext indicates that we’re setting the design-time DataContext for the view.
  • Binding Source={d:DesignInstance GlobalFiltersViewModel} instructs the designer in this case to use a GlobalFiltersViewModel instance for this view.
  • Converter={x:Static Global:DesignTimeViewModelLocator.Instance} means that we won’t directly use the GlobalFiltersViewModel as the DataContext, but will instead pass it to the DesignTimeViewModelLocator to be converted to the actual value we want to use as the DataContext. Note that I’m using a static property here since the project is a class library and doesn’t have a global app.xaml resource dictionary that I can use to create the DesignTimeViewModelLocator.

The C# implementation

using Caliburn.Micro;
using System;
using System.Linq;
using System.Windows.Data;
using System.Globalization;
using Global.Models;
 
namespace Global
{
    public class DesignTimeViewModelLocator : IValueConverter
    {
        public static DesignTimeViewModelLocator Instance = new DesignTimeViewModelLocator();
 
        private static readonly SimpleContainer container;
 
        static DesignTimeViewModelLocator()
        {
            if (!Execute.InDesignMode) return;
 
            AssemblySource.Instance.Clear();
            AssemblySource.Instance.Add(typeof(DesignTimeViewModelLocator).Assembly);
            container = new SimpleContainer();
            IoC.GetInstance = container.GetInstance;
            IoC.GetAllInstances = container.GetAllInstances;
            IoC.BuildUp = container.BuildUp;
 
            var viewModels = typeof(DesignTimeViewModelLocator).Assembly.DefinedTypes
               .Where(t => t.IsSubclassOf(typeof(PropertyChangedBase)));
            foreach (var vm in viewModels)
            {
                container.RegisterPerRequest(vm.AsType(), null, vm.AsType());
            }
 
            container.Singleton<IEventAggregator, EventAggregator>();
            container.Singleton<ITrace, DesignTimeTrace>();
        }
 
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            // Blend creates types from a runtime/dynamic assembly, so match on name/namespace
            var viewModelType = typeof(DesignTimeViewModelLocator).Assembly.DefinedTypes
                .First(t => t.Name == value.GetType().Name && value.GetType().Namespace.EndsWith(t.Namespace)).AsType();
 
            return container.GetInstance(viewModelType, null);
        }
 
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}
  • Use a static constructor to ensure this only runs once.
  • Configure IoC/Dependency Injection by registering all the ViewModels. (In my project, they all derive from PropertyChangedBase.)
  • Register the sample data you want to see in the designer. In this code, I’m using a DesignTimeTrace.
  • Implement IValueConverter and return an instance of the desired ViewModel from the IoC container.

DesignTimeViewModelLocator Benefits

This DesignTimeViewModelLocator technique combines the traditional ViewModelLocator approach with Dependency Injection to create arbitrarily complex ViewModels. It uses constructor injection to recursively create the necessary dependencies for any view. With Caliburn.Micro conventions, it will even bind and created nested child views. The XAML language service will also use the ViewModel type in the DesignInstance attribute to provide accurate intellisense in data binding expressions.

I’d encourage you to try using a DesignTimeViewModelLocator with Caliburn.Micro in your next XAML project and see if it doesn’t speed up the development of your UI components.

Footnotes

1. Note that the blend designer process can be quite finicky. Ideally, you should make small incremental changes and keep it working from the start of a project. Otherwise, you can attach the debugger to XDesProc.exe and investigate the exceptions being thrown.