2018-06-10

Using UserControls with Caliburn.Micro

It is common to want to create a reusable UserControl, to be placed into a WPF (Windows Presentation Foundation) screen. This can be done one of two ways:
  • ViewModel First
  • View First
The techniques below will show how to do both of these schemes using Caliburn.Micro to perform the plumbing to connect them up. It took me quite a bit of research to figure out how to make these happen, particularly the View first, scheme. Both of these techniques can be used to create a UserControl in a Window that was itself generated using the other technique. For example, a window that was created using the ViewModel First scheme can include a UserControl that is created using the View First scheme.

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.

No comments :

Post a Comment