ISerialized .Net, C#, Scrum and agile software development

15Jan/103

Getting started with Caliburn Part 2: Multiple Views on one ViewModel

The documentation and tutorials on Caliburn is still very limited, and is one of the biggest obstacles getting started with Caliburn right now. In this post I hope to fill some of the gaps I have seen in the lack of documentation by showing how you can easily hook up two Views to one ViewModel.
Caliburn is rapidly improving it's functionality, and by following Rob Eisenberg on Twitter at EisenbergEffect one can easily see that this is an alive framework with many new and exciting changes!

This is Part 2 of my series on Caliburn, Part 1 "Getting started with Caliburn" is found here, and Part 3 "Unit testing Caliburn in NUnit" can be found here.

This tutorial starts with the CaliburnCalculator created in part 1, where we here will add a second user control containing a slider which will use the same ViewModel (NumberInputViewModel) as our previous usercontrol with the text box.

We start by doing some minor changes to the NumberInputViewModel class to enable both ints and strings as input:

public class NumberInputViewModel: MultiPresenter, INumberInputViewModel
{
    private readonly IEventAggregator _eventAggregator;

    public NumberInputViewModel(IEventAggregator eventAggregator)
    {
        _eventAggregator = eventAggregator;
        Number = 5;
    }

    private string numberStr;
    public string NumberStr
    {
        get { return numberStr; }
        set
        {
            numberStr = value;
            NotifyOfPropertyChange("NumberStr");
            NotifyOfPropertyChange("CanSquareIt");
        }
    }

    private int number;
    public int Number
    {
        get { return number; }
        set
        {
            number = value;
            NumberStr = value.ToString();
            NotifyOfPropertyChange("Number");
            NotifyOfPropertyChange("CanSquareIt");
        }
    }

    public bool CanSquareIt
    {
        get
        {
            int result;
            return int.TryParse(NumberStr, out result);
        }
    }

    public void SquareIt()
    {
        _eventAggregator.Publish(Number);
    }
}

Worth to notice:

  • We change the Number property to NumberStr
  • We add new int property named Number
  • We change the content of the CanSquareIt method to test if the value NumberStr is a valid int
  • We initialize the Number property to eg. 5 at startup
  • And we now send the value of Number to the EventAggregator

You might also have noticed that we call the NotifyOfPropertyChanged on CanSquareIt on each change. The following flow can now be seen on invalid input:

  1. The user inputs a literal in the text box
  2. The View is binded to the ViewModel so the NumberStr property's setter is called with the litteral
  3. The setter populates the NumberStr with the new value
  4. It notifies that it has changed
  5. It notifies that the CanSquareIt must execute
  6. CanSquareIt will parse, find a problem and return false
  7. The SquareIt button will automatically be disabled as the CanSquareIt returne false (Notice: My example code here will not enable the view and SquareIt button again)

The CaliburnCalculator will now work as before, but I want a user control containing a slider to use the same ViewModel, namely my NumberInputViewModel. I therefore create a new user control called NIV2 which only contains a slider. The user control called NIV2 will have the following XAML:

<UserControl
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:ApplicationModel="clr-namespace:Caliburn.PresentationFramework.ApplicationModel;assembly=Caliburn.PresentationFramework" x:Class="CaliburnCalculator.Views.NIV2"
    mc:Ignorable="d" MinHeight="109" Width="511">
    <StackPanel>
        <Slider Minimum="0" Maximum="100" Interval="1" Name="SquareIt" VerticalAlignment="Center" Value="{Binding Number}"  />
        <ContentControl ApplicationModel:View.Context="Ctx1"
                        ApplicationModel:View.Model="{Binding}" />
    </StackPanel>
</UserControl>

A couple of things to notice here: I have defined a ContentControl with ApplicationModel:View.Context and ApplicaiontModel:View.Model where I create a Context called Ctx1 which defined the Context to use. Also notice that the sliders name is SquareIt, meaning it will call the SquareIt method directly every time we change it's value, and hence the square will be calculated instantly!

Right now our two Views are correctly designed, but theViewModel need some new attributes to startup both Views when loaded:

[Singleton(typeof(INumberInputViewModel))]
[View(typeof(NIV2), Context = "Ctx2")]
[View(typeof(NumberInputView), Context = "Ctx1")]

Here we say that we use two Views for this ViewModel, each with their own Context defined. We also change the super class to MultiPresenter.

We now only have one more step to do, to start using the new application. We have to change our ShellView to use the new Context defined, as well as change the size of our ShellView:

<Window x:Class="CaliburnCalculator.Views.ShellView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:cal="clr-namespace:Caliburn.PresentationFramework.ApplicationModel;assembly=Caliburn.PresentationFramework"
    Title="ShellView" Height="248" Width="561">
    <Grid>
        <StackPanel Name="LayoutRoot" Background="LightGray" >
            <ItemsControl ItemsSource="{Binding Presenters}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <ContentControl cal:View.Context="Ctx2"
                                        cal:View.Model="{Binding}" />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </StackPanel>
    </Grid>
</Window>

Notice that the ContentControl is very similar to our old version, the only difference is that we now set Context property to Ctx2. When you now fire up your new application it should look something like this:

Updating the text box automatically updates the slider, and updating the slider automatically updates the text box and the calculated square. A (very) simplified MVVM example with two Views connected to one ViewModel.

About Paul Eie

My name is Pål Eie (English alias: Paul Eie) I'm an IT consultant working in Stavanger, Norway. My experience spans from AIX to Windows and from embedded C to Uniface, Java, C++ and C#. My blog will involve everything that is related to technology. As long as it might be of interest to either newbies or seniors, I will blog about it! If you find some of my posts about basic old technology, it just means you know more than many of my other readers!
Comments (3) Trackbacks (0)
  1. A default view was not found for CaliburnCalculator.ViewModels.ResultViewModel.
    Views searched for include:
    CaliburnCalculator.Views.ResultViews.ICtx2
    CaliburnCalculator.Views.ResultViews.Ctx2
    CaliburnCalculator.ViewModels.ResultViewViews.ICtx2
    CaliburnCalculator.ViewModels.ResultViewViews.Ctx2
    CaliburnCalculator.ViewModels.ResultViews.ICtx2
    CaliburnCalculator.ViewModels.ResultViews.Ctx2

  2. Hi Paul, these were very helpful tutorials on Caliburn. I’m having the same error message shown above (part 2). I appreciate if you could shed some light…..Thanks.

  3. I found the solution:
    a) Add this attribute to ResultViewModel:
    [View(typeof(ResultView), Context = "Ctx2")]

    b) Change Slider View’s xaml definition to
    cal:Message.Attach=”[Event ValueChanged]=[Action SquareIt()]”

    c) You have to remove the Name=”SquareIt” from Silver views’ xaml. It seems this example was for Silverlight.

    d) Remove Interval property from Slider definition in Slider Views’ xaml. You can use SmallChange in silverlight.


Leave a comment


No trackbacks yet.

Blogglisten