Article summary
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 aGlobalFiltersViewModel
instance for this view.Converter={x:Static Global:DesignTimeViewModelLocator.Instance}
means that we won’t directly use theGlobalFiltersViewModel
as the DataContext, but will instead pass it to theDesignTimeViewModelLocator
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 theDesignTimeViewModelLocator
.
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. ↩
I am using your DesignTimeViewModelLocator implementation with great success with Blend for Visual Studio 2015, until I installed Visual Studio Update 1.
The Name and Namespace used for matching are now completely different and therefor the requested viewModelType can not be found.
Hi Berry,
I noticed the same thing yesterday. Unfortunately the design-time implementation occassionally changes which is why it can be useful to attach the debugger to XDesProc.exe. Try using the following code to find the matching the type:
var viewModelType = typeof(DesignTimeViewModelLocator).Assembly.DefinedTypes
.First(t => t.FullName == value.ToString()).AsType();
Hi Kayle,
I had already attached the debugger to XDesProc.exe where I discovered the different Name and Namespace, but I didn’t came up with the solution you suggested – which is working well.
Hi, Kayle!
Where is it supposed to declare the DesignTimeViewModelLocator?
The thing is that you define in that class an IoC-container and configure it. So, I suppose this is going to be in the ViewModel project? I feel that it is not good to initialize an IoC-container in a ViewModel project.
I don’t understand the meaning of the interface IType.
Could you give a small example of its usage?