Commands

This page covers the following topics:

What are commands

Commands provide a way to expose code within an application that performs an action (typically a method) so that it can be invoked by a user interaction with a UI element, such as clicking a Button.

For example the MainModel includes a public Save method:

public partial record MainModel()
{
    public void Save() { ... }
}

The Save method will be exposed as an IAsyncCommand (an MVUX interface that extends the ICommand interface) on the generated ViewModel for the MainModel:

public partial class MainViewModel
{
    public IAsyncCommand Save { get; }
}

The Command property on a Button can be bound to the Save command on the MainViewModel. When the Button is clicked, the Save command will be executed, which will invoke the Save method on MainModel.

<Button Command="{Binding Save}">Save</Button>

During the execution of the Save method, the Button will automatically be disabled, making it clear that the method is running.

Command generation

By default, MVUX will generate a command in the ViewModel for each public method in a Model via Implicit command generation. This behavior can be customized using attributes, or alternatively, can be disabled in favor of Explicit command creation.

Implicit command generation

Basic commands

By default, a command property will be generated in the ViewModel for each method on the Model that has no return value or is asynchronous (e.g. returns ValueTask or Task).

The asynchronous method on the Model may take a single CancellationToken parameter, which will be cancelled if the ViewModel is disposed of whilst commands are running. Although a CancellationToken parameter is not mandatory, it's a good practice to add one, as it enables the cancellation of the asynchronous operation.

For example, if the Model contains a method in any of the following signatures:

  1. A method without a return value:

    public void DoWork();
    
  2. A method returning ValueTask, with a CancellationToken parameter:

    public ValueTask DoWork(CancellationToken ct);
    
  3. A method returning ValueTask, without a CancellationToken parameter:

    public ValueTask DoWork();
    

a DoWork command will be generated in the ViewModel:

public IAsyncCommand DoWork { get; }

In some scenarios, you may need to use the method only, without a command generated for it. You can use the ImplicitCommands attribute to switch off or back on command generation for certain methods, classes, or assemblies. For example, in this code, the ImplicitCommand attribute has been used to disable the creation of the command for the DoWork method.

[ImplicitCommand(false)]
public ValueTask DoWork();

When command generation is switched off, the methods under the scope which has been switched off will be generated in the ViewModel as regular methods rather than as commands, meaning that they are still available to be data-bound or invoked via the ViewModel.

Using the CommandParameter

An additional parameter can be added to the method on the Model, which is then assigned with the value of the CommandParameter received from the View. For example, when a Button is clicked, the Button.CommandParameter value will be passed to the command. The CommandParameter value is first passed to the CanExecute method on the command to determine if the command can be executed. The command checks both that the CommandParameter value can be cast to the correct type, and that there's not already an invocation of the command for that CommandParameter value. Assuming CanExecute returns true, when the Button is clicked the Execute method on the command is invoked which routes the call, including the CommandParameter value (correctly cast to the appropriate type), to the method on the Model.

In this example the Model defines a method, DoWork, that accepts a parameter, param:

public void DoWork(double param) { ... }

The corresponding command in the ViewModel looks the same as before. However, the implementation, which you can inspect in the generated code, includes logic to validate the type of the CommandParameter, and subsequently passes the cast value to the DoWork method on the Model:

public IAsyncCommand DoWork { get; }

The command can be consumed in the View by setting the CommandParameter on the Button. In this case, the value is data bound to the Value property on the Slider:

<Slider x:Name="slider" Minimum="1" Maximum="100"/>
<Button Command="{Binding DoWork}" CommandParameter="{Binding Value, ElementName=slider}"/>

If the CommandParameter is null, or if its type doesn't match the parameter type of the method, the button will remain disabled. On the other hand, in case the CommandParameter is specified in the View but the method in the Model doesn't have a parameter, the View's CommandParameter value will just be disregarded.

It's also still recommended to include a CancellationToken, which will allow the method to be cancelled. For example, the preferred definition for the DoWork method would be asynchronous and include the CancellationToken parameter.

public ValueType DoWork(double param, CancellationToken ct) { ... }

Additional Feed parameters

The current value of any Feed can be materialized in an asynchronous method by awaiting the Feed:

public IFeed<int> MyFeed = ...;

public async ValueTask DoWork()
{
    int myFeedValue = await MyFeed;
}

However, MVUX commands also enable consuming the current value of Feed properties in the Model, using parameter names in the Model method, with a name and type matching the Feed property. The name matching is NOT case-sensitive.

For example:

public IFeed<int> CounterValue => ...

public void ResetCounter(int counterValue) { ... }

When the command is executed and the ResetCounter method is invoked, because the parameter counterValue matches a feed property in the Model by type and name, this parameter will be materialized with the actual most recent value from the Feed. Note that the type of the method parameter is the generic parameter type of the feed (in this case int), rather than the type of the feed property (so not IFeed<int>).

As before, it's recommended that the method be made asynchronous and include a CancellationToken parameter.

This behavior can be configured using the FeedParameter and ImplicitFeedCommandParameter attributes. For example, the implicit resolution of feed parameters has been disabled and an explicit feed parameter specified for the newValue parameter for the ResetCounter method.

public IFeed<int> CounterValue => ...

[ImplicitFeedCommandParameter(false)]
public void ResetCounter([FeedParameter(nameof(CounterValue))] int newValue) { ... }

Command generation rules

Here is a recap of the rules the Model method must comply with for an IAsyncCommand wrapper to be generated for it:

  • The method may be synchronous (void) or asynchronous (ValueTask/Task)
  • Any return values of the method (if any) will be discarded.
  • The method may have one CancellationToken parameter or none.
  • The method may have multiple parameters that can be resolved from feeds (see Additional Feed parameters above).
  • The method may have one parameter additional, other than parameters resolved from Feeds or CancellationToken, to be provided from the View's CommandParameter property.

Configuring command generation using attributes

ImplicitCommands attribute

By default, implicit command generation is enabled when the MVUX package is referenced. That means that any method in the Model that matches the command generation rules will have an accompanying command wrapper generated for it.

However, you may choose to enable, or disable, implicit command generation for a specific class, or assembly. Conversely, when implicit command generation has been disabled for an assembly, it can be enabled for specific classes.

Enabling, or disabling, implicit command generation can be achieved using the ImplicitCommands attribute.

Here is an example of disabling implicit command generation throughout an entire assembly:

[assembly:ImplicitCommands(false)]

and then enabling implicit command generation for a single class

[ImplicitCommands(true)]
public partial record MyModel(...)

Command attribute

In addition to the ImplicitCommands attribute which controls implicit command generation of a class or assembly, you can explicitly enable or disable the command generation for an individual method using the Command attribute. When command generation is disabled for a method, that method will still be generated in the ViewModel for the Model as a pass-through to the original method on the Model.

Assuming the ImplicitCommands attribute is used to disable implicit command generation for an assembly, or class, a command can be generated for the method by decorating it with the Command attribute (with its default value true):

[assembly:ImplicitCommands(false)]

[Command]
public async ValueTask DoWork()
{
}

Or on the contrary, if implicit command generation is enabled for an assembly or class, using the Command attribute will prevent the generation of a command. Instead, a regular method, in this case called DoWork will be created on the ViewModel.

[assembly:ImplicitCommands(true)]

[Command(false)]
public async ValueTask DoWork()
{
}

The Command attribute has precedence over the ImplicitCommands attribute. If a method is decorated with this attribute, whether or not a command is generated will depend solely on the Command attribute value.

One example of when you'd want to switch off command generation is if you are using x:Bind Event Binding, you will want to opt-out from command generation for the bound method so that you can bind directly to the method on the ViewModel from the View.

In this example, a Save method with the same signature as the Save method on the Model will be created on the ViewModel.

[Command(false)]
public async ValueTask Save () { ... }

The Click event on the Button can then be bound using x:Bind to the Save method on the ViewModel.

<Button Click="{x:Bind Save}">Save</Button>

ImplicitFeedCommandParameter attribute

You can opt-in or opt-out of implicit matching of Feeds and command parameters by decorating the current assembly or class with the ImplicitFeedCommandParameters attribute:

[assembly:ImplicitFeedCommandParameter(false)]

[ImplicitFeedCommandParameter(true)]
public partial record MyModel

FeedParameter attribute

You can also explicitly match a parameter with a Feed even if the names don't match. Decorate the parameter with the FeedParameter attribute to explicitly match a parameter with a Feed:

public IFeed<string> Message { get; }

public async ValueTask Share([FeedParameter(nameof(Message))] string msg) { ... }

ImplicitFeedCommandParameter and FeedParameter attributes can also be nested to enable or disable specific scopes in the app. The FeedParameter setting has priority over ImplicitFeedCommandParameter, so parameters decorated with FeedParameter will explicitly indicate that the parameter is to be fulfilled by a Feed.

Using XAML behaviors to execute a command when an event is raised

You can also utilize MVUX's generated commands and invoke them when an event is raised. This can be achieved with the XamlBehaviors library (Nuget packages Uno.Microsoft.Xaml.Behaviors.Interactivity.WinUI and Uno.Microsoft.Xaml.Behaviors.WinUI.Managed).

For example, if you want to capture a TextBlock being double-tapped, you can add in the Model a method to be invoked on that event:

public void TextBlockDoubleTapped(string text) { ... }

The TextBlockDoubleTapped method will be generated as a command, which you can then use XAML behaviors to invoke when the TextBlock's DoubleTapped event occurs. You can also pass its command parameter to the method (although you can choose to omit it):

<Page
    ...
    xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
    xmlns:interactions="using:Microsoft.Xaml.Interactions.Core">

    <TextBlock x:Name="textBlock" Text="Double-tap me">
        <interactivity:Interaction.Behaviors>
            <interactions:EventTriggerBehavior EventName="DoubleTapped">
                <interactions:InvokeCommandAction
                    Command="{Binding TextBlockDoubleTapped}"
                    CommandParameter="{Binding Text, ElementName=textBlock}"/>
            </interactions:EventTriggerBehavior>
        </interactivity:Interaction.Behaviors>
    </TextBlock>
</Page>

When the TextBlock is double-tapped (or double-clicked), the TextBlockDoubleTapped command which is generated in the ViewModel will be executed, and in turn, the TextBlockDoubleTapped method in the Model will be invoked. The text 'Double-tap me' will be passed in as the command parameter.

Explicit command creation

Adding commands via code generation is sufficient enough to cover most scenarios. However, sometimes you may need to have more control over the command creation, which is where explicit command creation is useful.

Commands can be created manually using the static class Command, which provides factory methods for creating commands.

Command.Async factory method

The Async utility method takes an AsyncAction callback as its parameter. An AsyncAction refers to an asynchronous method that has a CancellationToken as its last parameter (preceded by any other parameters), and returns a ValueTask.

public ICommand MyCommand => Command.Async(async(ct) => await PingServer(ct));

In the above example, PingServer is of the following signature:

ValueTask PingServer(CancellationToken ct);

The Command.Async method will create a command that when executed will run the PingServer method asynchronously.

Create & Create<T>

To create a command you can use the API provided in the Command.Create factory methods. The Command.Create provides an ICommandBuilder parameter which you can use to configure the command in a fluent-API fashion. This API is intended for Uno Platform's internal use but can come in handy if you need to create custom commands.

ICommandBuilder provides the three methods below.

  • Given

    This method initializes a command from a Feed (or a State). The command will be triggered whenever a new value is available to the Feed. It takes a single IFeed<T> parameter.

    public IFeed<int> PageCount => ...
    
    public IAsyncCommand MyCommand => Command.Create(builder => builder.Given(PageCount));
    
  • When

    Defines the 'can execute' of the command. It accepts a predicate of T, where T is the type the command has been created with. When this is configured, the command will be executed only if the condition is true.

    public IAsyncCommand MyCommand => Command.Create<int>(builder => builder.When(i => i > 10));
    

    In the above example, the predicate passed into the When method will be executed when the UI wants to determine if the command can be executed, which will only be true if the command parameter will be greater than 10.

  • Then

    Sets the asynchronous callback to be invoked when the Command is executed. This method will be generic if there's a preceding parameter setting (via Given or When).

    public IAsyncCommand MyCommand => Command.Create(builder => builder.Then(async ct => await ExecuteMyCommand(ct)));
    
    public ValueTask ExecuteMyCommand(CancellationToken ct)
    {
        ...
    }
    

    You can use the Execute instead of Then. These are just aliases of each other.

Example

Here's a complete example where the MyCommand is defined in the Model.

public IAsyncCommand MyCommand =>
    Command.Create(builder =>
        builder
        .Given(CurrentPage)
        .When(currentPage => currentPage > 0)
        .Then(async (currentPage, ct) => await NavigateToPage(currentPage, ct)));

public IFeed<int> CurentPage => ...

public ValueTask NavigateToPage(int currentPage, CancellationToken ct) { ... }

As with implicitly created commands, the Command property on UI controls, such as a Button, can be bound to the command, MyCommand. However, since commands created using the fluent API are not replicated on the ViewModel, the binding expression has to include the ViewModel's Model property to access the Model instance.

<Button Command="{Binding Model.MyCommand}" Content="Execute my command" />

In the above example (in the Model), when the button is clicked, the Given section will be materialized with the most recent value of the CurrentPage Feed, it will be then evaluated with the predicate provided in the When call, and if its value is greater than 0, it will be passed on to Then, and NavigateToPage will be called with the CurrentPage Feed value passed on.

This is a diagram detailing the factory methods in the Command class:

A class diagram of MVUX command builder inheritance structure

Below is a list of all methods and their signatures:

Methods of Command class:

Method name Signature
Async public static IAsyncCommand Async(AsyncAction execute, [CallerMemberName] string? name = null)
Create public static IAsyncCommand Create(Action build)
Create<T> public static IAsyncCommand Create(Action<ICommandBuilder> build, [CallerMemberName] string? name = null)

Methods of ICommandBuilder:

Method name Signature
Given public ICommandBuilder Given(IFeed parameter)
Then public void Then(AsyncAction execute)

Methods of ICommandBuilder<T>:

Method name Signature
When public IConditionalCommandBuilder When(Predicate canExecute)
Then public void Then(AsyncAction execute)

Methods of IConditionalCommandBuilder<T>:

Method name Signature
Then public void Then(AsyncAction execute);

AsyncAction refers to an action with a variable number of parameters (up to 16), with its last parameter being a CancellationToken, and returns a ValueTask:

public delegate ValueTask AsyncAction<in T1, T2...>(T1 t1, T2 t2 ... , CancellationToken ct);