Skip to content

commands

Frederic Vogels edited this page Feb 28, 2022 · 3 revisions

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 a RoutedEventArgs-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.

Disableable Commands

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
Print 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, whereas CanSave 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 and CanSave parts. Forget one CanSave 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.

Implementation

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.

Making use of commands

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.

Exercise

Add one Button to each side of the TextBox, labeled - and +. The most straightforward way to achieve this is to introduce a new Grid 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.

Exercise

  • Rename IncrementCommand to AddCommand (reminder: Visual Studio has refactoring capabilities.)
  • Rename TemperatureScaleViewModel's Increment to Add.
  • Add an extra field delta which represents the "direction". It is +1 for Increment and -1 for Decrement.
  • Perform the necessary changes to the XAML and TemperatureScaleViewModel so that everything works as intended.

Build and run.

Snapshot: add-command

Exercise

We only want to allow temperatures ranging from 0K to 1000K. Have CanExecute change to false 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