Using Generic Types for MVVM
The MVVM pattern seems to have become the defacto standard for implementing cool WPF applications.
Rob Eisenberg suggested using Conventions to help enforce a separation of View and ViewModel. This to me smacks of Magic Strings which is just not nice.
Lately I’ve been playing with a different method of doing this using XAML Generics.
I’d like to share this with the community and see how you all feel about this approach.
The basic idea is that all Views should derive from a ViewBase
For example:
Assume we have a ViewModel of type SomeViewModel and we want to create a view that represents it, all we have to do is create the following XAML:
<ve:ViewRoot x:Class="app.SomeView" x:TypeArguments="vm:SomeViewModel" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ve="clr-namespace:ViewEngine;assembly=ViewFramework" > </ve:ViewRoot>
and a Code Behind file:
public partial class SomeView : ViewRoot<SomeViewModel> { public SomeView() { InitializeComponent(); } }
And bingo… our application will use SomeView everywhere SomeViewModel occurs in the visual tree.
Because of the data binding system we can now build our view referencing the view model, so assuming there is a Title property in the view model we can write this to a label like this:
<ve:ViewRoot x:Class="app.SomeView" x:TypeArguments="vm:SomeViewModel" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ve="clr-namespace:ViewEngine;assembly=ViewFramework" > <Label Content="{Binding Title}"/> </ve:ViewRoot>
No naming conventions, no DataTemplate writing, just completely transparent intent.
Framework Wire-Up
Of course this doesn’t happen out of the box and requires a framework and a little global wiring up.
Let’s start with the simple bit, wiring it up, and then get to explaining how this works behind the scenes.
To make it simple I did away with the App.xaml startup system and went back to the old static main in Program.cs approach… I have no doubt it could be integrated into the app.xaml system if needed.
[STAThread] public static void Main() { var app = new Application(); ViewEngine.Initialise(app, Assembly.GetExecutingAssembly()); ViewEngine.Run(new WindowViewModel()); }
Simple huh?
Framework
Of course all the magic and challenge happens in the framework itself.
The basic principle is straightforward:
- Scan the provided assembly and find all subclasses of ViewRoot
.
- Set up mappings between the ViewClasses and their
models.
- Wrap those in DataTemplates.
- Load the data templates into the applications root ResourceDictionary.
The rest is handled by WPF for us.
There are however a couple of challenges to using Generics in WPF that make this more complex than one might expect.
Access to Properties
Not being able to access things like ResourceDictionary properties on the children of a generic type.
Fix: Create a 2 stage derivation of ViewRoot, the first called ViewRoot and the second called ViewRoot
public class RootView<T> : RootView { } public class RootView : ContentControl { }
Top Level Windows
Of course top level windows cannot be derived from ContentControl and must be derived from Window so we have to introduce some special case handling.
Its own assembly
As I discovered in one of my earlier posts on XAML it is important to build the ViewEngine in a separate assembly.
View Engine
Still it’s pretty plain sailing, in fact a whole ViewEngine class can be presented here. Obviously this isn’t commercial ready but it gives you a base to play with.
public interface IView { } internal interface IViewRoot : IView { } public class ViewRoot<T> : ViewRoot { } public abstract class ViewRoot : ContentControl, IViewRoot { } public class WindowRoot<T> : WindowRoot { } public abstract class WindowRoot : Window, IView { } public static class ViewEngine { private static Application sApp; public static void Initialise(Application app, params Assembly[] assembliesWithViews) { sApp = app; CreateViewViewModelMapping(assembliesWithViews); } public static Window Run(object viewModel) { var rootWindow = CreateRootWindow(viewModel); sApp.Run(rootWindow); return rootWindow; } private static void CreateViewViewModelMapping(IEnumerable<Assembly> assembliesWithViews) { foreach (var assemblyWithViews in assembliesWithViews) AddViewTypesToTemplates(assemblyWithViews.GetTypes()); } private static void AddViewTypesToTemplates(IEnumerable<Type> potentialViewTypes) { foreach (var potentialViewType in potentialViewTypes) if (TypeImplementsValidViewInterface(potentialViewType)) AddViewTypeMapping(potentialViewType); } private static bool TypeImplementsValidViewInterface(Type potentialViewType) { if (typeof(IView).IsAssignableFrom(potentialViewType)) return potentialViewType.BaseType.GetGenericArguments().Length > 0; return false; } private static void AddViewTypeMapping(Type viewType) { var modelType = viewType.BaseType.GetGenericArguments()[0]; if (typeof(IViewRoot).IsAssignableFrom(viewType)) { var template = new DataTemplate(modelType); var visualFactory = new FrameworkElementFactory(viewType); template.VisualTree = visualFactory; sApp.Resources.Add(template.DataTemplateKey, template); } else sApp.Resources.Add(modelType, viewType); } private static Type FindViewForModelType(Type modelType) { return sApp.Resources[modelType] as Type; } private static Window CreateRootWindow(object viewModel) { Type viewType = FindViewForModelType(viewModel.GetType()); if (viewType == null) throw new Exception(string.Format("No View for ViewModel type: {0}", viewModel.GetType().Name)); var view = Activator.CreateInstance(viewType); var window = view as Window; if (window == null) throw new Exception(string.Format("Could not initialise root WindowView({0})", viewModel.GetType().Name)); window.DataContext = viewModel; return window; } }
In case you also need an example MainWindow it is straightforward:
<ve:WindowRoot x:Class="app.MainWindow" x:TypeArguments="WindowViewModel" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ve="clr-namespace:ViewEngine;assembly= ViewEngine " Title="{Binding TitleProperty}" Height="300" Width="300" Content="{Binding ContentProperty}" > <ve:WindowRoot.Resources> </ve:WindowRoot.Resources> </ve:WindowRoot>
Have fun and do let me know if you find any way to make this better…
You must be logged in to post a comment.