Атрибут RelayCommand

Тип RelayCommand — это атрибут, позволяющий создавать свойства команды ретранслятора для аннотированных методов. Его цель состоит в том, чтобы полностью исключить шаблон, необходимый для определения команд, которые упаковывают частные методы в viewmodel.

Note

Чтобы работать, аннотированные методы должны находиться в частичном классе. Если тип вложен, все типы в дереве синтаксиса объявления также должны быть помечены как частичные. Если этого не сделать, возникнут ошибки компиляции, так как генератор не сможет создать другое частичное объявление этого типа с указанной командой.

API платформы:RelayCommand, ICommand, IRelayCommand, IRelayCommand<T>IAsyncRelayCommand, IAsyncRelayCommand<T>TaskCancellationToken

Принцип работы

Атрибут RelayCommand можно использовать для анотации метода в частичном типе, например:

[RelayCommand]
private void GreetUser()
{
    Console.WriteLine("Hello!");
}

И он создаст команду следующим образом:

private RelayCommand? greetUserCommand;

public IRelayCommand GreetUserCommand => greetUserCommand ??= new RelayCommand(GreetUser);

Note

Имя созданной команды будет создано на основе имени метода. Генератор будет использовать имя метода, добавит в конец "Command" и удалит префикс "On", если он есть. Кроме того, для асинхронных методов суффикс Async также удаляется перед добавлением команды.

Параметры команды

Атрибут [RelayCommand] поддерживает создание команд для методов с параметром. В этом случае она автоматически изменит созданную команду, чтобы она была IRelayCommand<T> вместо нее, принимая параметр того же типа:

[RelayCommand]
private void GreetUser(User user)
{
    Console.WriteLine($"Hello {user.Name}!");
}

Это приведет к следующему созданному коду:

private RelayCommand<User>? greetUserCommand;

public IRelayCommand<User> GreetUserCommand => greetUserCommand ??= new RelayCommand<User>(GreetUser);

Результирующая команда автоматически будет использовать тип аргумента в качестве аргумента типа.

Асинхронные команды

Команда [RelayCommand] также поддерживает оборачивание асинхронных методов через интерфейсы IAsyncRelayCommand и IAsyncRelayCommand<T>. Это обрабатывается автоматически, когда метод возвращает Task тип. Например:

[RelayCommand]
private async Task GreetUserAsync()
{
    User user = await userService.GetCurrentUserAsync();

    Console.WriteLine($"Hello {user.Name}!");
}

Это приведет к следующему коду:

private AsyncRelayCommand? greetUserCommand;

public IAsyncRelayCommand GreetUserCommand => greetUserCommand ??= new AsyncRelayCommand(GreetUserAsync);

Если метод принимает параметр, результирующая команда также будет универсальной.

Есть особый случай, когда у метода есть CancellationToken, так как он будет передан команде, чтобы обеспечить возможность отмены. То есть метод, как показано ниже.

[RelayCommand]
private async Task GreetUserAsync(CancellationToken token)
{
    try
    {
        User user = await userService.GetCurrentUserAsync(token);

        Console.WriteLine($"Hello {user.Name}!");
    }
    catch (OperationCanceledException)
    {
    }
}

Это приведёт к тому, что сгенерированная команда будет передавать токен в обёрнутый метод. Это позволяет потребителям просто вызывать IAsyncRelayCommand.Cancel, чтобы подать сигнал этому токену и обеспечить корректную остановку незавершённых операций.

Включение и отключение команд

Часто полезно отключить команды, а затем позже отменить состояние и снова проверить, можно ли выполнять команды. Для поддержки этого RelayCommand атрибут предоставляет CanExecute свойство, которое можно использовать для указания целевого свойства или метода для оценки возможности выполнения команды:

[RelayCommand(CanExecute = nameof(CanGreetUser))]
private void GreetUser(User? user)
{
    Console.WriteLine($"Hello {user!.Name}!");
}

private bool CanGreetUser(User? user)
{
    return user is not null;
}

Таким образом, CanGreetUser вызывается, когда кнопка впервые привязывается к пользовательскому интерфейсу (например, к элементу button), а затем снова вызывается каждый раз, когда для команды вызывается IRelayCommand.NotifyCanExecuteChanged.

Например, это способ привязки команды к свойству для управления состоянием:

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(GreetUserCommand))]
private User? selectedUser;
<!-- Note: this example uses traditional XAML binding syntax -->
<Button
    Content="Greet user"
    Command="{Binding GreetUserCommand}"
    CommandParameter="{Binding SelectedUser}"/>

В этом примере созданное SelectedUser свойство будет вызывать GreetUserCommand.NotifyCanExecuteChanged() метод при каждом изменении его значения. В пользовательском интерфейсе элемент управления Button привязан к GreetUserCommand, то есть каждый раз, когда возникает событие CanExecuteChanged, он снова будет вызывать свой метод CanExecute. Это приведёт к вычислению обёрнутого метода CanGreetUser, в результате чего будет возвращено новое состояние кнопки в зависимости от того, является ли входной экземпляр User (который в интерфейсе привязан к свойству SelectedUser) null или нет. Это означает, что всякий раз, когда изменяется SelectedUser, GreetUserCommand будет включаться или отключаться в зависимости от того, задано ли значение для этого свойства, что и является желаемым поведением в этом сценарии.

Note

Команда не будет автоматически отслеживать, когда изменилось возвращаемое значение метода или свойства CanExecute. Разработчик должен вызвать IRelayCommand.NotifyCanExecuteChanged, чтобы пометить команду как недействительную и запросить повторную проверку связанного метода CanExecute, а затем обновить визуальное состояние элемента управления, привязанного к этой команде.

Обработка параллельных выполнений

При асинхронной команде можно настроить решение о том, разрешать ли одновременные выполнения или нет. При использовании атрибута RelayCommand это можно задать с помощью AllowConcurrentExecutions свойства. По умолчанию используется значение false, то есть, пока выполнение не находится в состоянии ожидания, команда будет сигнализировать, что она отключена. Если вместо него задано trueзначение, любое количество одновременных вызовов можно поместить в очередь.

Обратите внимание, что если команда принимает маркер отмены, маркер также будет отменен при запросе параллельного выполнения. Основное различие заключается в том, что если разрешено параллельное выполнение, команда останется включенной, и она запустит новое запрошенное выполнение, не ожидая завершения предыдущего.

Обработка асинхронных исключений

Существует два способа обработки исключений асинхронных команд ретранслятора.

  • Ожидание и повторный выброс исключения (по умолчанию): когда команда ожидает завершения вызова, любые исключения будут естественным образом повторно выброшены в том же контексте синхронизации. Обычно это означает, что выбрасываемые исключения будут просто приводить к сбою приложения, что соответствует поведению синхронных команд (где выбрасываемые исключения также приводят к сбою приложения).
  • Передача исключений планировщику задач: если команда настроена на передачу исключений планировщику задач, генерируемые исключения не приведут к сбою приложения, а вместо этого будут доступны через предоставляемый IAsyncRelayCommand.ExecutionTask, а также будут передаваться вверх в TaskScheduler.UnobservedTaskException. Это позволяет более сложным сценариям (например, привязывать компоненты пользовательского интерфейса к задаче и отображать различные результаты в зависимости от результата операции), но более сложно использовать правильно.

Поведение по умолчанию заключается в том, что команды ожидают завершения и повторно выбрасывают исключения. Это можно настроить с помощью FlowExceptionsToTaskScheduler свойства:

[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task GreetUserAsync(CancellationToken token)
{
    User user = await userService.GetCurrentUserAsync(token);

    Console.WriteLine($"Hello {user.Name}!");
}

В этом случае try/catch не нужен, так как исключения больше не приведут к сбою приложения. Обратите внимание, что это также приведёт к тому, что другие, не связанные с этим исключения не будут автоматически выбрасываться повторно, поэтому следует тщательно продумать подход к каждому отдельному сценарию и соответствующим образом настроить остальной код.

Отмена команд для асинхронных операций

Еще один вариант для асинхронных команд — возможность запросить создание команды отмены. Это ICommand оболочка асинхронной команды ретранслятора, которую можно использовать для запроса отмены операции. Эта команда автоматически сигнализирует о своем состоянии в соответствии с тем, может ли она использоваться в любое время. Например, если связанная команда не может быть выполнена, ее состояние также будет указано как недоступное для выполнения. Это можно использовать следующим образом:

[RelayCommand(IncludeCancelCommand = true)]
private async Task DoWorkAsync(CancellationToken token)
{
    // Do some long running work...
}

Это также приведет к созданию свойства DoWorkCancelCommand. Затем это можно привязать к другому компоненту пользовательского интерфейса, чтобы пользователи могли отменить ожидающие асинхронные операции.

Добавление настраиваемых атрибутов

Как и при наблюдаемых свойствах, RelayCommand генератор также включает поддержку пользовательских атрибутов для созданных свойств. Чтобы воспользоваться этим, можно просто использовать цель [property: ] в списках атрибутов для аннотированных методов, а MVVM Toolkit передаст эти атрибуты сгенерированным свойствам команд.

Например, рассмотрим такой метод:

[RelayCommand]
[property: JsonIgnore]
private void GreetUser(User user)
{
    Console.WriteLine($"Hello {user.Name}!");
}

Будет создано свойство GreetUserCommand с атрибутом [JsonIgnore] над ним. Вы можете использовать столько списков атрибутов, предназначенных для метода, сколько нужно, и все из них будут перенаправляться в созданные свойства.

Примеры

  • Ознакомьтесь с примером приложения (для нескольких платформ пользовательского интерфейса), чтобы просмотреть набор средств MVVM в действии.
  • Дополнительные примеры можно найти в модульных тестах.