- ViewModel First
- View First
In the example code below, the main window View is called MainWindowView and has a ViewModel called MainWindowViewModel. The ViewModel First control has a ViewModel called ViewModelFirstTestControlViewModel, which is displayed with the View called ViewModelFirstTestControlView. The View First control has a View called ViewFirstTestControlView and has a ViewModel called ViewFirstTestControlViewModel.
The ViewModel first scheme places a ContentControl into the MainWindowView, with a x:Name attribute. For example:
<ContentControl x:Name="ViewModelFirstTestControlViewModel" />
The MainWindowViewModel then has this code:
namespace TestSystem.ViewModels { using Caliburn.Micro; /// <summary>A ViewModel for the main window.</summary> /// <seealso cref="T:Caliburn.Micro.PropertyChangedBase"/> public class MainWindowViewModel : PropertyChangedBase { /// <summary>Initializes a new instance of the <see cref="MainWindowViewModel"/> class.</summary> public MainWindowViewModel() { this.ViewModelFirstTestControlViewModel = new ViewModelFirstTestControlViewModel("ViewModel First Set Content"); } /// <summary>Gets the ViewModelFirst test control view model.</summary> /// <value>The ViewModelFirst test control view model.</value> public ViewModelFirstTestControlViewModel ViewModelFirstTestControlViewModel { get; private set; } } }
So the constructor of the MainWindowViewModel instantiates the ViewModel of the UserControl, passing any arguments to initialize the values in the control. A property with the same name as the x:Name of the ContentControl exposes that ViewModel to the ContentControl. When the ContentControl needs to display the ViewModel, Caliburn.Micro finds the appropriate View and displays that as the content of the ContentControl.
The content of the actual UserControl View in this example looks like this, but could be virtually anything you want:
<UserControl x:Class="TestSystem.Views.ViewModelFirstTestControlView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <StackPanel <TextBlock Text="{Binding Path=Caption}" /> </StackPanel> </UserControl>
The ViewModel for the control in this example look like this:
namespace TestSystem.ViewModels { using Caliburn.Micro; /// <summary>A ViewModel for the ViewModelFirst test control. This class cannot be inherited.</summary> /// <seealso cref="T:Caliburn.Micro.PropertyChangedBase"/> public sealed class ViewModelFirstTestControlViewModel : PropertyChangedBase { /// <summary>The caption.</summary> private string caption = "Default ViewModel first caption"; /// <summary> /// Initializes a new instance of the <see cref="ViewModelFirstTestControlViewModel"/> class.</summary> public ViewModelFirstTestControlViewModel() { } /// <summary> /// Initializes a new instance of the <see cref="ViewModelFirstTestControlViewModel"/> class.</summary> /// <param name="caption">The caption.</param> public ViewModelFirstTestControlViewModel(string caption) { this.caption = caption; } /// <summary>Gets or sets the caption.</summary> /// <value>The caption.</value> public string Caption { get { return this.caption; } set { if (value != this.caption) { this.caption = value; this.NotifyOfPropertyChange(() => this.Caption); } } } } }
The main point about the code is that there is a constructor that takes any initial values to be set for the control. You may not actually need the default constructor.
Now, let's examine how to do virtually the same thing, but do it View First. In the MainWindowView, there is this code to place the control into the View:
<ctl:ViewFirstTestControlView cm:Bind.Model="TestSystem.ViewModels.ViewFirstTestControlViewModel" Caption="View First Set Content" />
For this Xaml to work, two namespace must be defined:
xmlns:cm="http://www.caliburnproject.org" xmlns:ctl="clr-namespace:TestSystem.Views"
The cm namespace comes from the Caliburn.Micro project. Many people use "cal" instead of "cm", but I've got a namespace for "calendrics" in some of my projects, so use cm instead. The "ctl" namespace is where your views reside.
The cm:Bind.Model specifies the ViewModel for the control. The Caption passes in the initial value of the control.
This retrieves the View for the control. The View looks very similar the the ViewModel First View, with some additions:
<UserControl x:Class="TestSystem.Views.ViewFirstTestControlView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:vm="clr-namespace:TestSystem.ViewModels" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <UserControl.Resources> <vm:ViewFirstTestControlViewModel x:Key="ViewFirstTestControlViewModel" /> </UserControl.Resources> <StackPanel x:Name="root" DataContext="{StaticResource ViewFirstTestControlViewModel}"> <TextBlock Text="{Binding Path=Caption}" /> </StackPanel> </UserControl>
The additions specify the ViewModel for the control as a resource and binds the DataContext of the first child control to that ViewModel. However, with View First, the thing you can't avoid is having code behind. The code behind for the UserControl looks like this:
namespace TestSystem.Views { using System.Windows.Controls; using TestSystem.ViewModels; /// <summary>A view first test control view.</summary> /// <seealso cref="T:System.Windows.Controls.UserControl"/> /// <seealso cref="T:System.Windows.Markup.IComponentConnector"/> public partial class ViewFirstTestControlView : UserControl { /// <summary>The view model.</summary> private ViewFirstTestControlViewModel vm; /// <summary>Initializes a new instance of the <see cref="ViewFirstTestControlView"/> class.</summary> public ViewFirstTestControlView() { this.InitializeComponent(); this.vm = (ViewFirstTestControlViewModel)this.root.DataContext; } /// <summary>Gets or sets the caption.</summary> /// <value>The caption.</value> public string Caption { get { return this.vm.Caption; } set { this.vm.Caption = value; } } } }
The code behind does the InitializeComponent(), then sets the ViewModel to the DataContext that was set in the view. This, in turn, is used to have the property of the control talk to the ViewModel. The ViewModel of the control looks like this:
namespace TestSystem.ViewModels { using Caliburn.Micro; /// <summary>A ViewModel for the ViewFirst test control. This class cannot be inherited.</summary> /// <seealso cref="T:Caliburn.Micro.PropertyChangedBase"/> public sealed class ViewFirstTestControlViewModel : PropertyChangedBase { /// <summary>The text.</summary> private string caption = "Default View first caption"; /// <summary>Gets or sets the text.</summary> /// <value>The text.</value> public string Caption { get { return this.caption; } set { if (value != this.caption) { this.caption = value; this.NotifyOfPropertyChange(() => this.Caption); } } } } }
This is almost the same as the ViewModel of the ViewModel First control, except it does not need the constructors, since the property is changed from the MainWindowView. (It has a default constructor that does nothing.)
A zip file for the entire project is found here. Included are all the files, including the Caliburn.Micro bootstrapper that sets up the files.
If you know of more efficient ways of doing any of the things I've described, please let me know in the comments.