The Navigation Model Pattern

View a video of this demo. Download version 2.0.3 of Update Controls and the demo source code to follow along.

Navigation Model Intent
The Navigation Model Pattern removes dependencies between view objects and makes UI state available to presentation logic.

Use the Navigation Model Pattern when controls interact with one another in a non-trivial manner. For example, selecting an object in a list displays details in a grid. Or checking a checkbox enables an associated control.

Problem
WPF makes it really easy to bind a property of one control to a property of another. For example, if the selected item in a list box becomes the data context for a grid, the code might look like this:

<ListBox ItemsSource="{Binding People}" x:Name="personListBox">
	<!-- ... -->
</ListBox>

<Grid DataContext="{Binding ElementName=personListBox, Path=SelectedItem}">
	<!-- ... -->
	<Label Grid.Row="0" Grid.Column="0" Content="First Name:"/>
	<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding First}"/>
	<!-- ... -->
</Grid>

But direct control-to-control data binding causes trouble. Your UI is no longer composable, since controls directly reference one another. It is difficult to perform presentation logic on control properties, since the presentation model would have a reverse dependency upon the view. And it is difficult to programmatically set a control property based on user action.

Solution
Instead of binding controls directly to one another, move all of the user selection state into a Navigation Model. The navigation model is one shared location where user selection state resides. All controls that use this shared state bind to this one place. The controls don't know about one another. Any control can set the selection state, and any control can consume it. When the selection changes, all controls are updated.

Create a navigation model class
The navigation model is just a class. It has properties that correspond to the user's current selections. It has no persistent storage for this state. It survives only as long as the user's session.

In the example that we've been building, the user can select a Person. The resulting navigation model looks like this:

public class NavigationModel
{
    private Person _selectedPerson;

    #region Independent properties
    // Generated by Update Controls --------------------------------
    private Independent _indSelectedPerson = new Independent();

    public Person SelectedPerson
    {
        get { _indSelectedPerson.OnGet(); return _selectedPerson; }
        set { _indSelectedPerson.OnSet(); _selectedPerson = value; }
    }
    // End generated code --------------------------------
    #endregion
}

To generate this class, declare just the field. Select the field and press Ctrl+D, G. The Update Controls add-in will generate the property and the Independent sentry.

Expose the navigation model through the presentation model
The presentation model is a thin, transparent wrapper around the data and navigation models. It adds presentation logic where necessary, but does not hide these raw models from the view.

The presentation model initializes a reference to the navigation model in its constructor, and exposes that reference as a property. It also uses that reference in other presentation properties.

public class PresentationModel
{
    private PersonList _personList;
    private NavigationModel _navigationModel;

    public PresentationModel(PersonList personList, NavigationModel navigationModel)
    {
        _personList = personList;
        _navigationModel = navigationModel;
    }

    public PersonList PersonList
    {
        get { return _personList; }
    }

    public NavigationModel NavigationModel
    {
        get { return _navigationModel; }
    }

    public string Title
    {
        get { return "People - " +

                (_navigationModel.SelectedPerson != null ?
                    _navigationModel.SelectedPerson.Name : ""); }
    }
}

References to the data model and presentation model are not generated using Ctrl+D, G. These models don't change, so there is no need to inject Independent sentries for change tracking.

Connect controls to the navigation model
The view can access navigation model properties through the presentation model's reference. Connect the SelectedItem property of the list box to the navigation model to allow the user to change it. Connect the DataContext property of the details grid to the navigation model so that it responds to user selection.

<ListBox ItemsSource="{u:Update PersonList.People}" SelectedItem="{u:Update NavigationModel.SelectedPerson}">
	<!-- ... -->
</ListBox>

<Grid DataContext="{u:Update NavigationModel.SelectedPerson}">
	<!-- ... -->
	<Label Grid.Row="0" Grid.Column="0" Content="First Name:"/>
	<TextBox Grid.Row="0" Grid.Column="1" Text="{u:Update First}"/>
	<!-- ... -->
</Grid>

We'll want some controls to become enabled only when conditions are right. To facilitate this, we add a boolean IsPersonSelected property to the navigation model. Be sure to use the SelectedPerson property, not the _selectedPerson field, so that we get the benefit of change tracking.

public class NavigationModel
{
    private Person _selectedPerson;

    #region Independent properties // ...

    public bool IsPersonSelected
    {
        get { return SelectedPerson != null; }
    }
}

Connect this property to the IsEnabled property of selected controls. For entire groups of controls, we wrap the group in a container, and connect the property of the container to the boolean. We can't use the existing container, because it changes its own data context.

<Button Content="Delete" IsEnabled="{u:Update NavigationModel.IsPersonSelected}" Click="DeleteButton_Click" />

<StackPanel IsEnabled="{u:Update NavigationModel.IsPersonSelected}">
	<Grid DataContext="{u:Update NavigationModel.SelectedPerson}">
		<!-- ... -->
	</Grid>
</StackPanel>

Consequences
While this pattern decouples view components to make them more composable, it does so at the cost of injecting code where once only markup was necessary. This means that it is difficult for a designer to express the behavior of an application without involving a developer.

To mitigate this cost, designers and developers should agree on a contract beforehand. Obvious properties, like SelectedPerson, should be added to the navigation model immediately. Less obvious properties, like IsPersonSelected, can be added afterward. It is very difficult to refactor in a navigation model after view components have been constructed, so the architecture should start with this pattern in place.

It is also troublesome that setting the DataContext of a control makes it impossible to get back to the presentation model. If detail controls need access to presentation logic or navigation state, then an additional presentation/navigation layer must be injected. This will be demonstrated in a future post.

Leave a Reply

You must be logged in to post a comment.