You can define a use-case-specific ViewModel as named OCL expressions and bind a WPF screen to it; this tutorial is for developers building the Set up new hockey game use case.
What this example builds
The example uses a declarative ViewModel named GameSetup. It presents one Game and provides the values needed to configure it:
- the game presentation text;
- a game-type pick list;
- home-team and visitor-team pick lists that depend on the selected game type;
- image values for the selected teams; and
- Boolean values that control whether the user can start or end the game.
A declarative ViewModel is a transformation layer between the domain model and a particular use case. Instead of putting rules such as “which teams are valid for this game type?” in XAML, you express that rule in the ViewModel with OCL. The UI binds to the resulting named values.
For the broader reasons for keeping use-case logic outside the UI, see Training:The ViewModel and Documentation:ViewModel for Business.
Model scenario
The root object is a new Game. A game has a selected GameType, a home team, a visitor team, and a scheduled date. A game type is associated with the team types that may participate. Teams belong to team types.
For example, the supplied test data defines these game types:
| Game type | Permitted team types |
|---|---|
| 15 years, Boys | Boys 15 years; Girls 15 years |
| 16 years, Boys | Boys 15 years; Boys 16 years; Girls 15 years |
| 15 years, Girls | Girls 15 years |
When the user selects 16 years, Boys, the home and visitor pick lists must contain teams whose team type is one of the permitted types. This is the rule that belongs in the ViewModel, not in the ComboBox definition.
Define the GameSetup ViewModel
Create a ViewModel named GameSetup for the game use case. The original example defines it as a series of named OCL expressions. Some expressions are nested: the team pick-list expressions depend on the game-type selection.
The ViewModel must expose the following binding contract to the WPF view:
| ViewModel value | Used by the UI | Purpose |
|---|---|---|
GameSetup
|
Presentation, GameType, Home, Visitor, image values, and state values
|
The ViewModel class representing the root game being edited. |
GameType_PickListPresentation
|
Game-type ComboBox.ItemsSource
|
The available game types. |
Home_PickListPresentation
|
Home-team ComboBox.ItemsSource
|
Teams valid for the selected game type. |
Visitor_PickListPresentation
|
Visitor-team ComboBox.ItemsSource
|
Teams valid for the selected game type. |
Home_Image
|
Home-team Image.Source
|
The image for the selected home team. |
Visitor_Image
|
Visitor-team Image.Source
|
The image for the selected visitor team. |
CanStartGame
|
Start button IsEnabled
|
Whether the game state permits starting the game. |
CanEndGame
|
End button IsEnabled
|
Whether the game state permits ending the game. |
Use names that describe what the UI receives. For example, Home_PickListPresentation communicates that the value is a presentation-ready list for the home-team selector. The view should not recreate the selection rule by filtering teams itself.
The CanStartGame and CanEndGame values follow the Game state machine. The ViewModel exposes the current state decision as a Boolean; the WPF view only uses it to enable or disable its controls.
Add representative test data
Before building the UI, create data that exercises the selection rule. The following excerpt creates game types, team types, valid combinations, and teams, then returns the new game that becomes the root object.
private Game CreateSomeTestData()
{
// Game types
var gtboys15 = new GameType(_es) { Name = "15 years, Boys" };
var gtboys16 = new GameType(_es) { Name = "16 years, Boys" };
var gtgirls15 = new GameType(_es) { Name = "15 years, Girls" };
// Team types
var ttb15 = new TeamType(_es) { Name = "Boys 15 years" };
var ttb16 = new TeamType(_es) { Name = "Boys 16 years" };
var ttg15 = new TeamType(_es) { Name = "Girls 15 years" };
// Valid team-game combinations
gtgirls15.TeamTypes.Add(ttg15);
gtboys15.TeamTypes.Add(ttb15);
gtboys15.TeamTypes.Add(ttg15);
gtboys16.TeamTypes.Add(ttb15);
gtboys16.TeamTypes.Add(ttb16);
gtboys16.TeamTypes.Add(ttg15);
new Team(_es) { Name = "Brynäs", Image = GetImage(imagebrynäs), TeamType = ttb15 };
new Team(_es) { Name = "Brynäs", Image = GetImage(imagebrynäs), TeamType = ttb16 };
new Team(_es) { Name = "Brynäs", Image = GetImage(imagebrynäs), TeamType = ttg15 };
new Team(_es) { Name = "Luleå", Image = GetImage(imageluleå), TeamType = ttb15 };
new Team(_es) { Name = "Luleå", Image = GetImage(imageluleå), TeamType = ttb16 };
new Team(_es) { Name = "Luleå", Image = GetImage(imageluleå), TeamType = ttg15 };
new Team(_es) { Name = "Djurgården", Image = GetImage(imagedjurgården), TeamType = ttb15 };
new Team(_es) { Name = "Djurgården", Image = GetImage(imagedjurgården), TeamType = ttb16 };
new Team(_es) { Name = "Djurgården", Image = GetImage(imagedjurgården), TeamType = ttg15 };
return new Game(_es)
{
ScheduledDate = DateTime.Now
};
}
This data intentionally includes the same club at multiple team-type levels. That makes the dependent list meaningful: a team is eligible because of its associated team type, not only because its name appears in a list.
Bind the WPF view
In the WPF window resources, create a ViewModelContent. Set ViewModelName to GameSetup and provide the EcoSpace type. Give the resource a key so that it can be assigned as a DataContext.
<Window.Resources>
<ecoVM:ViewModelContent
x:Key="VM1"
ViewModelName="GameSetup"
EcoSpaceType="{x:Type ecospace:WPFBindingEcoSpace}" />
<local:ImageBlobConverter x:Key="ImageBlobConverter" />
</Window.Resources>
<Grid DataContext="{StaticResource VM1}">
<!-- Controls bind to the GameSetup ViewModel here. -->
</Grid>
WPF uses the inherited DataContext when a binding has no explicit source. Setting it on this grid makes the GameSetup ViewModel available to controls below the grid in the logical tree.
Bind values and selection lists
The following cleaned-up excerpt shows the bindings used by the example. DisplayMemberPath="Name" displays the team or game-type name. SelectedValuePath="self" makes the selected object itself the selected value, which is then bound to the corresponding property on GameSetup.
<TextBlock Grid.Row="0" Grid.Column="0"
Text="GAME : " HorizontalAlignment="Right" />
<TextBox Grid.Row="0" Grid.Column="1"
Text="{Binding Path=Class[GameSetup]/Presentation, Mode=OneWay}" />
<TextBlock Grid.Row="1" Grid.Column="0"
Text="Type of game : " HorizontalAlignment="Right" />
<ComboBox Grid.Row="1" Grid.Column="1"
DisplayMemberPath="Name"
ItemsSource="{Binding Path=Class[GameType_PickListPresentation]}"
SelectedValuePath="self"
SelectedValue="{Binding Path=Class[GameSetup]/GameType}" />
<TextBlock Grid.Row="2" Grid.Column="0"
Text="Home team : " HorizontalAlignment="Right" />
<ComboBox Grid.Row="2" Grid.Column="1"
DisplayMemberPath="Name"
ItemsSource="{Binding Path=Class[Home_PickListPresentation]}"
SelectedValuePath="self"
SelectedValue="{Binding Path=Class[GameSetup]/Home}" />
<Image Grid.Row="2" Grid.Column="2"
Source="{Binding Path=Class[GameSetup]/Home_Image,
Mode=OneWay,
Converter={StaticResource ImageBlobConverter}}" />
<TextBlock Grid.Row="3" Grid.Column="0"
Text="Visitor team : " HorizontalAlignment="Right" />
<ComboBox Grid.Row="3" Grid.Column="1"
DisplayMemberPath="Name"
ItemsSource="{Binding Path=Class[Visitor_PickListPresentation]}"
SelectedValuePath="self"
SelectedValue="{Binding Path=Class[GameSetup]/Visitor}" />
<Image Grid.Row="3" Grid.Column="2"
Source="{Binding Path=Class[GameSetup]/Visitor_Image,
Mode=OneWay,
Converter={StaticResource ImageBlobConverter}}" />
<Button Grid.Row="5" Grid.Column="0"
IsEnabled="{Binding Path=Class[GameSetup]/CanStartGame}"
Click="ButtonStartGame_Click">Start Game</Button>
<Button Grid.Row="5" Grid.Column="1"
IsEnabled="{Binding Path=Class[GameSetup]/CanEndGame}">End Game</Button>
The image bindings are one-way because the UI displays the selected team image; it does not edit the image. The ImageBlobConverter converts the image value to a WPF image source.
Supply the EcoSpace and root object
After the window resources are available, connect the ViewModelContent to the EcoSpace and assign the game returned by the test-data method as its root object.
var viewModelContent = Resources["VM1"] as Eco.ViewModel.WPF.ViewModelContent;
viewModelContent.SetEcoSpace(_es);
viewModelContent.RootObject = CreateSomeTestData();
The root object is a dependency property, so it can participate in binding as both target and source. In this example, assigning the new Game supplies the object on which GameSetup evaluates its expressions.
Verify the use case
- Start the window with the test data loaded.
- Select a game type, such as 16 years, Boys.
- Open the home and visitor pick lists. Confirm that they contain only teams with team types permitted by that game type.
- Select a home team and a visitor team. Confirm that each corresponding image changes with the selected team.
- Change the game type and confirm that the dependent pick lists reflect the new valid team types.
- Check the Start Game and End Game buttons in each relevant Game state. Their enabled state must follow
CanStartGameandCanEndGame, rather than rules implemented in the WPF view.
Keep the ViewModel independent of presentation
The XAML controls and layout are replaceable. The ViewModel remains the definition of what data is available, what selections are valid, and when an operation is allowed. A WPF-focused designer can change the layout and styling without moving the team-eligibility rule or the Game state decision into the UI.
If the standard generated layout is sufficient, you can add placing hints to the ViewModel and use a ViewModelWPFUserControl rather than hand-writing the layout. Placing hints describe relative placement and available data; they do not make the ViewModel a presentation design. See Training:Taking It Further Still.
For ViewModel editing and generated forms, see Training:Bootcamp:Chapter 3. For placing containers and responsive layout, see Training:Bootcamp:Chapter 8.
See also
- Training:The ViewModel
- Documentation:ViewModel for Business
- Training:Taking It Further Still
- Training:Bootcamp:Chapter 5
- Documentation:Declarative ViewModels and Taborder
ViewModel State Lifecycle and Actions
ViewModel State Lifecycle and Actions
You can use the GameSetup ViewModel to keep the WPF view focused on bindings while you handle user choices, validation, and model updates through actions; this section is for developers extending the hockey-game setup use case.
Capture UI choices in the ViewModel
A binding connects a UI control to a named ViewModel value. In this example, the game-type ComboBox binds its selected value to Class[GameSetup]/GameType, and the home and visitor selectors bind to Class[GameSetup]/Home and Class[GameSetup]/Visitor.
When the user selects 16 years, Boys, GameType is the selected game type. The ViewModel expressions can then supply Home_PickListPresentation and Visitor_PickListPresentation with teams permitted by that type. The lists also exclude the team selected for the opposite side. The WPF view consumes those values; it does not repeat the filtering rule.
Keep use-case-specific transformation and checks in the ViewModel. Put logic that is not unique to this use case in the domain model so that other use cases can reuse it. For example, the ViewModel can determine whether the Start button is enabled, while the domain model owns the operation that starts a game.
Validate before an action changes the model
Use an action's EnableExpression to decide whether the action can run. An EnableExpression is an OCL expression that returns true or false and cannot change domain objects.
For the game example, expose the state decision as CanStartGame and bind it to the Start button's IsEnabled. The documented behavior is that Start is enabled only after both Home and Visitor are set, and End remains disabled until the game has started.
Validation also belongs in the logic that prepares parameters and performs the operation. UI selections may be in a presentation format, so transform them to model scope and check that the parameters sent to a domain-model method are valid. For example, before an action starts a game, validate the selected home and visitor teams according to the game rule rather than relying only on a disabled button.
Invoke model behavior with actions
An action can execute an Extended Action Language (EAL) expression. The available context depends on the action type:
| Action type | Context | GameSetup example |
|---|---|---|
| Global action | No object context. The expression can start from model classes, for example X.allinstances or X.Create.
|
Open a ViewModel-defined UI whose root object is supplied by its ViewModelRootObjectExpression. |
| ViewModel action (ContextAction) | The ViewModel context, including vCurrent_TheViewModelClassName variables. These follow selections in ViewModel grids.
|
Act on the game or selected object represented by the current ViewModel context. |
| Class action | The class object, referenced as self.
|
Call a method defined by the Game class on the current game.
|
For a button placed in a ViewModel, set its Is action setting to an available ViewModel action. For example, a Start button can invoke a ViewModel action whose ExecuteExpression calls the appropriate method on the current Game. A button with no action expression does not perform an operation.
ViewModel actions are unique to the view. Class actions are associated with a model class and can appear where an object of that class is shown. Use the Action editor to control the ViewModel level where a ViewModel action appears; an action that is relevant to a selected team should not also appear at unrelated levels of the ViewModel tree.
Build a multi-step workflow
An EAL ExecuteExpression can call more than one method and perform checks between calls. Use this when the use case requires a sequence that is specific to the ViewModel rather than a single reusable domain operation.
For example, the setup flow can be organized as:
- The user selects a game type, Home team, and Visitor team through bindings.
- The ViewModel exposes whether the game can start through
CanStartGame. - The user invokes the Start action.
- The action uses its context to call the required model behavior on the game.
- The ViewModel state values determine that Start is no longer available and that End becomes available when the game state permits it.
Use ViewModel variables when an action needs a reference to an object the user is working on. vCurrent refers to the active ViewModel class object, while self depends on the location of the expression. In action language expressions, selfVM refers to the running ViewModel and can execute an action, execute persistent-storage work, rerun the query plan, inspect changed objects in the view, and work with access groups or security.
Initialize a variable that requires an expression in an init action that runs once, or in the OnShow expression of the action that navigates to the view. Do not place an expression in a variable initial value.
Keep declarative expressions focused
Keep the GameSetup expressions focused on the presentation values the UI needs: permitted teams, team images, and Boolean state decisions. For example, Home_PickListPresentation should provide the eligible home teams instead of making the WPF ComboBox filter all teams itself.
Use OCL for read-oriented ViewModel values and EnableExpressions. Use EAL ExecuteExpressions for operations that change domain objects. An EnableExpression cannot have side effects.
