Hands-on MVVM 4: the View Model layer

I presented this material at Dallas XAML on May 4, 2010. Please download the demo code and follow along. This post takes us half way the project called “Step4”. The next one will take us the rest of the way, but then the numbers will be out of sync.

In part 3, we added a “Display As” feature to the system. In doing so, the data model started getting messy. This time, we’ll pull those view-specific features out of the data model.

The data model has a job to do. It evaluates domain logic. That’s already enough for a single class. We don’t want to further burden it with managing a view. Robert C. Martin tells us of the Single Responsibility Principle. A class should have only one reason to change. If you’re changing the data model, it’s because you are modifying the domain. You shouldn’t also change it when modifying the behavior of a view. So let’s move that behavior out of the data model. The place it goes is the View Model. We data bind to the view model rather than to the data model.

View-specific properties
The Title property of the Person class exists to serve the view. It’s not the title of a person; it’s the title of a window that displays a person. This is the first to go.

public class PersonViewModel
{
    private Person _person;

    public PersonViewModel(Person person)
    {
        _person = person;
    }

    public string Title
    {
        get
        {
            return "Person - " + _person.DisplayUsingStrategy(_person.DisplayAs);
        }
    }
}

The Person View Model takes a reference to a Person. It uses the Person to provide the desired behavior.

Pass-through properties

The view displays text boxes where the user can edit the properties of a person directly. Since the view is no longer bound directly to the data model, we have to pass these properties through the view model.

public class PersonViewModel
{
    private Person _person;

    public PersonViewModel(Person person)
    {
        _person = person;
    }

    public string FirstName
    {
        get { return _person.FirstName; }
        set { _person.FirstName = value; }
    }

    public string LastName
    {
        get { return _person.LastName; }
        set { _person.LastName = value; }
    }

    public string Email
    {
        get { return _person.Email; }
        set { _person.Email = value; }
    }

    public string Phone
    {
        get { return _person.Phone; }
        set { _person.Phone = value; }
    }

    public string Title
    {
        get
        {
            return "Person - " + _person.DisplayUsingStrategy(_person.DisplayAs);
        }
    }
}

No data is stored in the view model itself. It all passes through to the data model.

One view model per DataContext

The “Display As” combo box has an ItemsSource that is data bound to a collection. Each item in this collection is the DataContext of one combo box item. Before it was a string. Now we’ll make it a view model of its own.

public class DisplayStrategyViewModel
{
    private Person _person;
    private DisplayStrategy _displayStrategy;

    public DisplayStrategyViewModel(Person person, DisplayStrategy displayStrategy)
    {
        _person = person;
        _displayStrategy = displayStrategy;
    }

    public string Display
    {
        get { return _person.DisplayUsingStrategy(_displayStrategy); }
    }
}

Again, this view model passes its behavior through to the Person object. But now it displays that person according to a given display strategy. We use a DataTemplate to show that property in the combo box.

<ComboBox ItemsSource="{Binding DisplayAsOptions}" SelectedItem="{Binding DisplayAs}">
    <ComboBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Display}"/>
        </DataTemplate>
    </ComboBox.ItemTemplate>
</ComboBox>

And now we need to generate these view models.

Project through a factory

The simplest way to generate a collection of view models is to use linq to project a collection of data objects through a view model constructor. In this case, we are listing all of the display options, so we project the values of the DisplayStrategy enumeration. We add this property to the Person View Model.

public IEnumerable<DisplayStrategyViewModel> DisplayAsOptions
{
    get
    {
        return Enum.GetValues(typeof(DisplayStrategy))
            .OfType<DisplayStrategy>()
            .Select(displayStrategy =>
                new DisplayStrategyViewModel(_person, displayStrategy));
    }
}

From the list of DisplayStrategy values, we produce a list of Display Strategy View Models that render the person using different options.

ItemsSource/SelectedItem duality

Finally, the user needs to select one of the display strategies from the combo box. The SelectedItem property provides this behavior. It is the dual of the ItemsSource property: its type must be the same as the IEnumerable generic parameter.

public DisplayStrategyViewModel DisplayAs
{
    get { return new DisplayStrategyViewModel(_person, _person.DisplayAs); }
    set { _person.DisplayAs = value.DisplayStrategy; }
}

This property wraps the DisplayAs selection in a view model so that it agrees with the type of the list.

Equals and GetHashCode

If you were to run the code now, you would see that the window comes up initially blank. But when you select a “Display As” setting, it appears correctly. The problem is that the view cannot find the SelectedItem in the ItemsSource collection. For each property, we created a new Display Strategy View Model. These are two different instances of similar objects. To tell the view that they are indeed the same, we need to implement Equals and GetHashCode in DisplayStrategyViewModel.

public override bool Equals(object obj)
{
    if (obj == this)
        return true;
    DisplayStrategyViewModel that = obj as DisplayStrategyViewModel;
    if (that == null)
        return false;
    return _displayStrategy == that._displayStrategy;
}

public override int GetHashCode()
{
    return _displayStrategy.GetHashCode();
}

Now the view recognizes that they are the same, so the window appears correctly. You must always implement Equals and GetHashCode when you create an ItemsSource/SelectedItem dual.

We have successfully moved the view-specific behavior out of the data model and into a view model. We pass all of the properties that are data-bound to the view through to the data model. By projecting related objects through a constructor, we can keep a view model layer in between the view and the data model at all times.

The application works now, but there are a few places that no longer update. That’s because we didn’t implement INotifyPropertyChanged on the view model to tell the view about dependent properties. In part 4, we’ll add INotifyPropertyChanged to get that behavior back.

Leave a Reply

You must be logged in to post a comment.