Selection Model
View a video of this demo. Download version 2.0.3 of Update Controls and the demo source code to follow along.
Intent
The Selection Model Pattern removes dependencies between view objects and makes UI state available to presentation logic.
Use the Selection 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 Selection Model. The selection 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 selection model class
The selection 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 selection model looks like this:
{
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 selection model through the presentation model
The presentation model is a thin, transparent wrapper around the data and selection 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 selection model in its constructor, and exposes that reference as a property. It also uses that reference in other presentation properties.
{
private PersonList _personList;
private SelectionModel _selectionModel;
public PresentationModel(PersonList personList, SelectionModel selectionModel)
{
_personList = personList;
_selectionModel = selectionModel;
}
public PersonList PersonList
{
get { return _personList; }
}
public SelectionModel SelectionModel
{
get { return _selectionModel; }
}
public string Title
{
get { return "People - " +
}
}
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 selection model
The view can access selection model properties through the presentation model's reference. Connect the SelectedItem property of the list box to the selection model to allow the user to change it. Connect the DataContext property of the details grid to the selection model so that it responds to user selection.
<ListBox ItemsSource="{u:Update PersonList.People}" SelectedItem="{u:Update SelectionModel.SelectedPerson}"> <!-- ... --> </ListBox> <Grid DataContext="{u:Update SelectionModel.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 selection model. Be sure to use the SelectedPerson property, not the _selectedPerson field, so that we get the benefit of change tracking.
{
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 SelectionModel.IsPersonSelected}" Click="DeleteButton_Click" /> <StackPanel IsEnabled="{u:Update SelectionModel.IsPersonSelected}"> <Grid DataContext="{u:Update SelectionModel.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 selection model immediately. Less obvious properties, like IsPersonSelected, can be added afterward. It is very difficult to refactor in a selection 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 selection state, then an additional presentation/selection layer must be injected. This will be demonstrated in a future post.
Recent comments
3 years 13 weeks ago
3 years 19 weeks ago
3 years 44 weeks ago
3 years 46 weeks ago
3 years 47 weeks ago
4 years 3 days ago
4 years 3 days ago
4 years 17 weeks ago
4 years 17 weeks ago
4 years 21 weeks ago