-
Notifications
You must be signed in to change notification settings - Fork 24
commands
We have yet to discuss an important concept in WPF: commands. Earlier in this tutorial, we made use of buttons. We relied on the Click
event to react to a button click:
In XAML:
<Button Click="OnClick" />
In corresponding .cs:
private void OnClick(object sender, RoutedEventArgs args)
{
...
}
Putting the logic dealing with a button click in MainWindow.cs
violates the layering principles discussed earlier. The logic belongs in the model, and the view model should provide an easy way for the view-layer to access it.
It is certainly possible to simply add code to OnClick
that calls the view model:
private void OnClick(object sender, RoutedEventArgs args)
{
viewModel.PerformSomeAction();
}
But we might as well link the button straight with the view model instead of going through an intermediate OnClick
. Now, you might think there's a way to tell WPF to look for the OnClick
in the view model instead of the .cs
file associated with the XAML, but that would be wrong:
- The
OnClick
method receives aRoutedEventArgs
-typed argument, which is WPF specific. It would violate the layering to infect the view model with this type. - The designers decided to add a bit of extra functionality to it which cannot be implemented by linking to a single method.
We now discuss this bit of extra functionality.
It often occurs in a GUI that certain buttons/menu items/... are disabled because they do not make sense in that particular context. For example, in a text editor, the following actions can only be performed when certain conditions are met:
Action | Condition |
---|---|
Save | Document has been modified since last save |
The document is nonempty | |
Undo | User has performed action in the past |
Redo | User has not performed any action since last undo |
Technically, we can already implement this behavior:
<Button Content="Save"
Click="Save"
IsEnabled="{Binding CanSave.Value} />
IsEnabled
is a Button
property that, if bound to a false
value, grays out the button and makes it unclickable. However, this approach has a number of shortcomings:
-
Save
is a method that would reside in the.xaml.cs
file, i.e., not in the view model, whereasCanSave
does reside there. It's rather unclean to separate related functionality like that. - Often, there's not just one way to save. There might be a button, a toolbar button, a menu item, a keyboard binding, ... For each of these you'd need to repeat the
Save
andCanSave
parts. Forget oneCanSave
check and you might have a crash on your hands.
A simple solution is to package the action (saving) with the condition (can the user save?) into a single object. This way, they are actually grouped in the same module and not spread across different layers. Also, since you would bind to this object as a whole, you can never forget about the condition.
This action+condition package is exactly what a Command
is.
In essence, a Command
is a "disableable function". Let's try to define an interface ICommand
. No need to copy-paste this, ICommand
already exists; this is merely an explanation of how it works.
interface ICommand
{
???
}
First, we need to implement the actual action (e.g., saving, printing, undoing, redoing.) This corresponds to a method. Let's call it Execute
:
interface ICommand
{
void Execute();
}
Now we need to add a way to indicate whether the command can be executed:
interface ICommand
{
void Execute();
bool CanExecute { get; }
}
There's one thing still missing: CanExecute
can change value at any time, and when it does, WPF needs to redraw the controls, e.g. turn them gray. Technically, we could make use of cells:
interface ICommand
{
void Execute();
Cell<bool> CanExecute { get; }
}
The real ICommand
is slightly different:
-
Cell
is our own invention. - WPF adds a bit of flexibility by adding an extra
object
parameter. We'll show you how to use it later.
The real ICommand
is
interface ICommand
{
void Execute(object parameter);
bool CanExecute(object parameter);
event EventHandler CanExecuteChanged;
}
To implement save functionality, you would create a SaveCommand
class which implements ICommand
. Next, you expose a SaveCommand
object in your view model:
class DocumentViewModel
{
public SaveCommand Save { get; }
}
Finally, you can bind multiple controls to it using the Command
property:
<Button Command="{Binding Save}" Content="Save" />
<MenuItem Command="{Binding Save}" Header="Save" />
<KeyBinding Modifiers="Ctrl" Key="S" Command="{Binding Save}" />
All these will together be enabled/disabled when CanExecute
switches to true
/false
. Clicking on any of them will cause Execute
to be called.
Right now, there are no buttons in our projects, so we will introduce some so as to be able to show how to define commands.
Let's add -
and +
buttons left and right of each text box. These buttons would allow to decrement and increment the temperature by one.
Add one
Button
to each side of theTextBox
, labeled-
and+
. The most straightforward way to achieve this is to introduce a newGrid
inside the already existing one with three columns. The buttons must be sized 32 × 32 while the text box should fill the remaining width.
Snapshot: buttons
Time to extend our view model. Open ViewModel.cs
and add the following code:
public class TemperatureScaleViewModel
{
private readonly ConverterViewModel parent;
private readonly ITemperatureScale temperatureScale;
public TemperatureScaleViewModel(ConverterViewModel parent, ITemperatureScale temperatureScale)
{
this.parent = parent;
this.temperatureScale = temperatureScale;
this.Temperature = this.parent.TemperatureInKelvin.Derive(kelvin => temperatureScale.ConvertFromKelvin(kelvin), t => temperatureScale.ConvertToKelvin(t));
+ this.Increment = new IncrementCommand(this.Temperature);
}
public string Name => temperatureScale.Name;
public Cell<double> Temperature { get; }
+ public ICommand Increment { get; }
}
+ public class IncrementCommand : ICommand
+ {
+ private readonly Cell<double> cell;
+ public IncrementCommand(Cell<double> cell)
+ {
+ this.cell = cell;
+ }
+ public event EventHandler CanExecuteChanged;
+ public bool CanExecute(object parameter)
+ {
+ return true;
+ }
+ public void Execute(object parameter)
+ {
+ cell.Value = Math.Round(cell.Value + 1);
+ }
}
In the XAML, link the button and the command:
<Button Grid.Column="2"
Content="+"
+ Command="{Binding Increment}"
Style="{StaticResource buttonStyle}" />
Run the application to check if the +
button works.
Snapshot: increment
We could now define a second command DecrementCommand
, but it would be identical to IncrementCommand
save for one detail. A better solution would be to generalize IncrementCommand
.
- Rename
IncrementCommand
toAddCommand
(reminder: Visual Studio has refactoring capabilities.)- Rename
TemperatureScaleViewModel
'sIncrement
toAdd
.- Add an extra field
delta
which represents the "direction". It is+1
forIncrement
and-1
forDecrement
.- Perform the necessary changes to the XAML and
TemperatureScaleViewModel
so that everything works as intended.
Build and run.
Snapshot: add-command
We only want to allow temperatures ranging from 0K to 1000K. Have
CanExecute
change tofalse
whenever incrementing/decrementing would cause the temperature to move outside this range.Don't forget to deal with
CanExecuteChanged
! You can check if it works by moving the slider: position it to the left and the-
button should become grayed out. Then move it to the right:-
should be enabled and+
disabled.
Snapshot: can-execute