Diving Back into WPF: UI Events and RelayCommands

WPF exposes a lot of UI events…most of which cannot be directly tied to RelayCommands because the events are not ICommands1.

But there is a workaround. Provided you add the Microsoft.Xaml.Behaviors.Wpf nuget package to your codebase.

That package adds a bunch of “behaviors”, some of which can transform a plain old Net event into an ICommand. Here’s an example (details omitted for clarity):

<mah:MetroWindow x:Class="J4JSoftware.GeoProcessor.ProcessWindow"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                 xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
                 xmlns:behaviors="http://schemas.microsoft.com/xaml/behaviors"
                 xmlns:local="clr-namespace:J4JSoftware.GeoProcessor"
                 Title="Process File" 
                 Height="250" Width="500">

    <behaviors:Interaction.Triggers>
        <behaviors:EventTrigger EventName="Loaded" 
                                SourceObject="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}">
            <behaviors:InvokeCommandAction Command="{Binding WindowLoadedCommand}"/>
        </behaviors:EventTrigger>
    </behaviors:Interaction.Triggers>

Line 7 lets you reference the behaviors elsewhere in the XAML file.

The magic takes place in lines 12 – 17 where I bind a WPF event — Loaded — to a RelayCommand in my viewmodel (WindowLoadedCommand, in this case).

Line 14 may or may not be required (I’m not sure) but it seems like a worthwhile thing to do. It defines the object raising the event being bound to a RelayCommand. In the case of the Loaded event that’s the window object itself.

Once you’ve set this up in the XAML file the viewmodel implementation is the same as always: define a RelayCommand property in your viewmodel and a handler for it (details omitted for clarity):

public ProcessorViewModel(
    IAppConfig appConfig,
    IUserConfig userConfig )
{
    _appConfig = appConfig;
    _userConfig = userConfig;

    WindowLoadedCommand = new AsyncRelayCommand( WindowLoadedCommandAsync );
}

public ICommand WindowLoadedCommand { get; }

private async Task WindowLoadedCommandAsync()
{
    _appConfig.APIKey = _userConfig.GetAPIKey( _appConfig.ProcessorType );

    if( string.IsNullOrEmpty( _appConfig.APIKey ) )
    {
        ProcessorState = ProcessorState.Aborted;
        Phase = "Processing not possible";
        return;
    }
            
    ProcessorState = ProcessorState.Ready;
    PointsProcessed = 0;
    Messages.Clear();
    OnPropertyChanged( nameof(Messages) );
            
    await Dispatcher.Yield();
    _cancellationSrc = new CancellationTokenSource();

    await ProcessAsync( _cancellationSrc.Token );
}

A few things worth noting from the code snippet:

  • Line 8 shows that there’s an asynchronous version of RelayCommand available in the MVVM toolkit. It’s useful when the handler method must be async (this handler method launches the lengthy process of snapping GPS coordinates to roads via the Google or Microsoft snap-to-route services).
  • Line 27 is another example of having to call OnPropertyChanged() directly because we’ve updated a property other than the one we’re "inside".
  • Line 29 is an example of an important call you’ll need to scatter through your codebase..at least, you will if you want your UI to be responsive :). It allows the UI update thread to run when it might not otherwise be able to for an extended period of time.
  • Line 30 sets up the ability of the various async methods that will be called next to be cancelled (in this app, that happens if the user clicks an Abort button which isn’t shown here).


  1. in fact, I don’t think any event is an ICommand — I think ICommands are defined separately from events 

Archives
Categories