Up until this point, we’ve relied exclusively on data-binding for the link between our view-models and our views. While data-binding handles a lot of scenarios well, it doesn’t support buttons, menu items and so on.

We could take an approach based on experience with WinForms, using event handlers to respond to button clicks, and so on.

Fortunately, WPF offers a much better approach - one that allows for better separation of concerns and testability: command-binding.

Hooking up a command for the “Add” button on our VocabularyBrowserView will require three steps: Configuring a command on the button, providing an implementation of the command on our view model, and wiring the two together.

Configuring the command

Our Add button has a Command property that we can use to associate a selected command with the button.

The WPF framework includes many predefined commands but none of them are quite right for our situation. The closest are the New and Save commands from ApplicationCommands, but we want to keep those for whole vocabulary lists, not individual words. (If you’re curious, you might also want to check out the predefined commands on ComponentCommands, EditingCommands, MediaCommands, and NavigationCommands).

Instead, we’ll define our own.

public static class ItemCommands
{
    public static RoutedCommand Delete { get; }
        = new RoutedCommand("delete", typeof(ItemCommands));

    public static RoutedCommand New { get; } 
        = new RoutedCommand("new", typeof(ItemCommands));

    public static RoutedCommand Save { get; }
        = new RoutedCommand("save", typeof(ItemCommands));
}

Each property has a unique identity made up of the provided name and the declaring class. This allows similar properties declared on different classes to be told apart.

We now add the New command to our button:

<!-- We need this namespace declaration at the top of the file -->
xmlns:desktop="clr-namespace:WordTutor.Desktop"

<!-- The button may now reference the command -->
<Button Content="Add"
        HorizontalAlignment="Center"
        Width="120"
        Grid.Row="12"
        Grid.Column="1"
        Command="desktop:ItemCommands.New">
</Button>

Implementing the command

On our VocabularyBrowserViewModel, we add a method for the command to trigger:

public void AddWord()
    => _store.Dispatch(new OpenNewWordScreenMessage());

With all our application logic centred in the model, our view model command implementations should all remain this straightforward.

Now we wrap that method using a RoutedCommandSink, converting the method (which is easy to test) into a command that can be linked to our button.

// Add this property
public RoutedCommandSink AddWordCommand { get; }

// and configure it from our constructor
AddWordCommand =
    new RoutedCommandSink(ItemCommands.New, AddWord);

Notice the naming convention we’re using here - the property is named for the method, with the suffix Command included; we’ll start enforcing the convention in a later post.

Wiring up the command

We now have a command configured on the button and an implementation of that command available on our view-model.

To wire them together, we need to add a CommandBinding to the CommandBindings collection of our view.

We could manually create each CommandBinding and add it to the collection ourselves.

But since every view will need the same wiring to occur, we’ll instead add the necessary logic to ViewFactory. This will ensure that all our commands are wired up automatically for every view.

After creating the view and assigning its DataContext, we add any extra command bindings required:

var result = (ContentControl)_container.GetInstance(viewType);
result.DataContext = viewModel;
result.CommandBindings.AddRange(CreateCommandBindings(viewModel));

The CreateCommandBindings() method looks for any properties with names ending Command and uses those values.

private static ICollection CreateCommandBindings(
    ViewModelBase model)
{
    var commands =
        from property in model.GetType().GetProperties()
        where property.Name.EndsWith(
            "Command", StringComparison.Ordinal)
        let command = property.GetValue(model)
            as RoutedCommandSinkBase
        where command != null
        select command.CreateBinding();
    var result = commands.ToList();
    return result;
}

We iterate over all of the properties on the model, looking for those with a name ending in Command, and from each of those, creating a command binding.

Running the application, you can now press the Add button and bring up a new screen.

Next time, we’ll put a few more commands to work.

Prior post in this series:
Debugging word selection
Next post in this series:
C# 8 and .NET Core 3.0

Comments

blog comments powered by Disqus