Composing View Models
From the early days of object oriented programming, software engineers quickly learned one very important concept: always prefer composition over inheritance. In fact, if you pay close attention to the Gang of Four Design Patterns, you’ll realize that they are primarily concerned with using composition in different ways. Inheritance isn’t “the ultimate evil” but it is the strongest form of coupling possible in almost any language; and it’s often one of the most prominent features of the language. This means building a compositional system often requires more diligence on the part of the development team. Fortunately, striving to build loosely coupled systems generally results in designs that are easier to evolve over time and easier to maintain.
You probably know all this and it may even be your standard operational mode for creating your entities and services. But do you follow this principle in your presentation tier? Actually, there’s a good chance you do … at least a little. You probably break down complex views into User Controls that are composed together. This technique has been enabled by the platform long before XAML. But how does this relate to separated presentation patterns such as MVVM? Should you follow the same strategy with your View Models? Yes.
Since Cocktail leverages Caliburn.Micro for its MVVM support, you have access to a core set of functionality designed from the beginning around composing and decomposing views and view models. Over the years, as I built various UIs, I eventually came to the conclusion that the key benefit of objects was not necessarily in their re-use but in their ability to simplify maintenance. I tried to infuse as much of that learning, particularly as it relates to XAML, into the core of Caliburn.Micro so that others could leverage it. In the remainder of this article we’re going to talk about the two main flavors of MVVM (View-First and View-Model-First) and see how they relate to composition. Of course, we’ll look at the core support for both of these approaches in Caliburn.Micro, both in terms of framework usage as well as how they work under the covers.
View-First
When I say “View-First” I’m not referring to the workflow of the designer or the developer. This doesn’t have to do with whether or not you build your View or your VM first. It has to do with what happens at runtime. In the View-First approach, the View is created first at runtime and thus controls the construction of the View-Model. This places the View-Model in a subservient relationship to the View. This is the normal mode that all XAML platforms support. Implementing View-First is very easy. It looks something like this:
public partial class CustomerView : UserControl { public CustomerView() { InitializeComponent(); DataContext = new CustomerViewModel(); } }
This is the way most developers handle UserControls. You can see here that the VM is created in the constructor of the View and set to the DataContext. This is the most basic implementation of View-First. You could use an IoC container to resolve the VM, then set it to the DataContext, or some other mechanism for actually acquiring the VM, but it’s still View-First because the View must exist before the VM and it is the View that is “choosing” which VM to use.
View-Model-First
In View-Model-First, the VM exists before the View and it is the VM which controls the creation of the View. Thus, the View is in a subservient relationship to the VM. Would it surprise you if I said that XAML also natively supported this approach? Well, it does. DataTemplates are an implementation of View-Model first. In this case, your Model or View-Model exists before the View. The platform uses your VM to select a View. The various XAML runtimes support this in differing degrees. The nicest implementation is probably WPF’s DataTemplateSelector combined with its support for implicit DataTemplates. In all cases, the View-Model “chooses” the View.
Shortcomings
If XAML already has these features built-in, why bother with Caliburn.Micro? As you probably guessed from the title of this section, there are a number of shortcomings in the platform support. To begin, the fact that there are two completely different code paths for these strategies is a problem. As a result, the framework is larger and less consistent, and you the developer require additional cognitive overhead to learn and understand two ways of doing essentially the same thing. Caliburn.Micro can’t really simplify or shrink the existing framework, but it does try to create a consistent API and approach to these two strategies as best as the underlying platform will allow. One of the big problems with View-First is that the developer has no control of View creation. Similarly with the platform support for View-Model-First, while this lets you control the VM creation, the underlying platform still usurps View creation. In Caliburn.Micro we couldn’t really fix the View-First shortcomings, but we were able to provide a better implementation of View-Model-First with additional smarts and hooks for you. What this means is that the platform will do the work for you, but you have the capability to reach in and take over when you need to. And though we couldn’t completely fix View-First, we were able to make a few improvements.
Digging Deeper
Let’s take a look at how View-Model-First composition works. To enable this, Caliburn.Micro has a special attached property called View.Model. You can create a ContantControl and set a binding on this property to invoke the View-Model-First composition engine. Your XAML would look like this:
<ContentControl cal:View.Model="{Binding APropertyThatReferencesAViewModel, Mode=TwoWay}" />
What does this do? Well, when the property is attached (View.Model) and the ContentControl is loaded, we look at the value of the bound property (APropertyThatReferencesAVewModel) and do the following:
Hook #1
We pass the value (your View-Model) to our ViewLocator which invokes a convention-based View lookup. We do a simple name mapping based on established industry patterns. So, if you’ve got CustomerViewModel, we create an instance of CustomerView. There are three important differences from how DataTemplates work:
1. This is convention based, so you don’t have to go configure all your DataTemplates in a ResourceDictionary. – Less work for you.
2. This works directly with UserControls, so you can have a single View per file, which is usually more difficult with DataTemplates. – More maintainable for you.
3. The ViewLocator is completely customizable. You can take full control of View creation by replacing our entire implementation or just adding a custom rule or two for your specific scenarios. – More control for you … or you could think of it as less road blocks.
Hook #2
Once your View is created it gets passed to our ViewModelBinder. This piece sets up the DataContext for you. But, there are lots of advantages to it otherwise:
1. We set up the DataContext but also provide some hooks that enable non-MVVM architectures such as Supervising Controller and Passive View. – Less work and more flexibility for you.
2. We apply a set of conventions to simplify databinding and commanding. – Less XAML in your views and less “glue-code” in your View-Models.
3. The ViewModelBinder is completely customizable. You can take full control and replace the entire implementation or you can add to or change existing conventions. – More control and less roadblocks.
With these two hooks you have complete control over how your UI is constructed and presented. We provide sensible defaults but you have the ability to reach in and customize every aspect of the process. That’s much better than basic DataTemplate support.
So, what about View-First? Well, we actually have a sister property to View.Model called Bind.Model. Since we can’t actually interrupt any of the XAML platforms and control view creation, this property doesn’t leverage the ViewLocator at all. Instead, it assumes that the element you have declared the property on is your View. It then looks at the value of the attached property and assumes that is your View-Model (or if it is a string, it uses your IoC container to look up the View-Model by key … effectively enabling a very powerful ViewModelLocator if you prefer that approach). Finally, it passes both of these objects along to the ViewModelBinder enabling its full set of features to work in View-First mode.
The Ultimate Convention
Up to now we’ve been talking about different approaches to MVVM and how Caliburn.Micro supports those. But what about this thing called View-Model Composition? When we use this term, we are usually using it with reference to a View-Model-First approach, because we are going to compose a very complex screen or component out of several (sometimes many) smaller VMs. We’ll then leverage Caliburn.Micro’s View.Model property to perform view location and convention binding across all VMs in the aggregate. This would normally involve a fair amount of work, but with Caliburn.Micro it’s trivial … in fact there’s no work at all thanks to the “Ultimate Convention”.
To understand what I’ve recently dubbed the “Ultimate Convention” and to get a better understanding of View-Model Composition, let’s take a look at a sample View-Model and View. Here’s the View-Model:
public class ShellViewModel { public CommandBarViewModel CommandBar { get; private set; } public object ActiveDocument { get; private set;} public ObservableCollection<object> Documents { get; private set; } public StatusViewModel Status { get; private set; } public ShellViewModel(CommandBarViewModel commandBar, StatusViewModel status) { CommandBar = commandBar; Status = status; Documents = new ObservableCollection<object>(); } }
This ViewModel represents the shell of our application. Notice that it is an aggregate of several other View-Models. This is View-Model Composition. It’s very simple. Just use the language features available to you to composed objects; features such as associations and collections. The magic happens when Caliburn.Micro applies its conventions to the View. Here’s the View for this VM:
<UserControl x:Class="BlogWeek3.ShellView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <DockPanel> <ContentControl x:Name="CommandBar" DockPanel.Dock="Top" /> <ContentControl x:Name="Status" DockPanel.Dock="Bottom" /> <ListBox x:Name="Documents" DockPanel.Dock="Left" /> <ContentControl x:Name="ActiveDocument" /> </DockPanel> </UserControl>
Caliburn.Micro has a special understanding of ContentControl and ItemsControl (and derivatives such as ListBox). When CM encounters one of these controls named the same as an association (ContentControl) or a collection (ItemsControl) it not only adds the binding to that property, but also sets its ContentTemplate (ContentControl) or ItemTemplate (ItemsControl) to a default template. The default template it uses is:
<DataTemplate xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:cal="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro"> <ContentControl cal:View.Model="{Binding}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" IsTabStop="False" /> </DataTemplate>
Notice the presence of the View.Model attached property. This effectively allows us to set up a recursive composition pattern. This special DataTemplate tells the XAML engine to call back into Caliburn.Micro and allows it to resolve and bind the View … which in turn will apply conventions on that child view … which might trigger more composition … etc. I hope you get the idea. This is a powerful mechanism because it allows you to decompose both your View-Models and your Views. You could use an IoC tool to re-compose your View-Models and leverage Caliburn.Micro to re-compose your Views from the constructed aggregate View-Model. There are many projects that I’ve worked on where the entire application is a single aggregate View-Model composed by an IoC container (including dynamically loaded components) and the View is effortlessly composed by Caliburn.Micro. The cool thing is that you can effortlessly flow in and out of View-Model-First Composition and View-First Composition in your application depending on what works best and leverage the appropriate technique to best suit your needs on a component by component basis.
Screen Composition Patterns
There’s a lot to get a grasp on in this article, so I don’t want to add too much more. Suffice it to say, once you start composing your screens as aggregate View-Models you will start to see some patterns emerge. Caliburn.Micro has a few interfaces and base classes around certain compositional patterns which I and others have seen recur in real applications. These patterns are around things like navigation, multi-document interfaces and other common shell and screen metaphors. If you are interested, there’s a pretty in depth article on the subject.
Recent comments ( 1 )
Rob, very nice article and explains everything very well. Very good reading!
Follow