Partager via


RelayCommand, attribut

Le type RelayCommand est un attribut qui permet de générer des propriétés de commande de relais pour des méthodes annotées. Son but est d’éliminer complètement le code réutilisable nécessaire pour définir les commandes enveloppant les méthodes privées d’un viewmodel.

Remarque

Pour fonctionner, les méthodes annotées doivent se trouver dans une classe partielle. Si le type est imbriqué, tous les types figurant dans l’arborescence de syntaxe de déclaration doivent également être annotés comme étant partiels. Faute de quoi, des erreurs de compilation se produiront, car le générateur ne peut pas générer une autre déclaration partielle de ce type avec la commande demandée.

API de plateforme :RelayCommand, ICommand, IRelayCommand, IRelayCommand<T>, IAsyncRelayCommand, IAsyncRelayCommand<T>, Task, CancellationToken

Fonctionnement

L’attribut RelayCommand peut être utilisé pour annoter une méthode dans un type partiel, comme ceci :

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

Et il génère une commande comme ceci :

private RelayCommand? greetUserCommand;

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

Remarque

Le nom de la commande générée est créé à partir du nom de la méthode. Le générateur reprend le nom de la méthode qu’il fait suivre « Command », puis supprime le préfixe « On », le cas échéant. Par ailleurs, pour les méthodes asynchrones, le suffixe « Async » est également supprimé préalablement à l’ajout de « Command ».

Paramètres de commande

L’attribut [RelayCommand] prend en charge la création de commandes pour les méthodes dotées d’un paramètre. Dans ce cas, il modifie automatiquement la commande générée pour en faire un IRelayCommand<T>, acceptant ainsi un paramètre du même type :

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

Il en résulte le code généré suivant :

private RelayCommand<User>? greetUserCommand;

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

La commande obtenue utilise automatiquement le type de l’argument comme argument de type.

Commandes asynchrones

La commande [RelayCommand] prend également en charge l’enveloppement de méthodes asynchrones, via les interfaces IAsyncRelayCommand et IAsyncRelayCommand<T>. Cela est géré automatiquement chaque fois qu’une méthode retourne un type Task. Exemple :

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

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

Voici le code qui en résulte :

private AsyncRelayCommand? greetUserCommand;

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

Si la méthode accepte un paramètre, la commande obtenue est également générique.

Il existe un cas particulier où la méthode dispose d’un CancellationToken, celui-ci étant propagé à la commande pour permettre l’annulation. Ainsi, dans le cas de la méthode suivante :

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

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

la commande générée transmet un jeton à la méthode enveloppée. Cela permet aux consommateurs d’appeler simplement IAsyncRelayCommand.Cancel pour signaler ce jeton et aux opérations en attente de s’arrêter correctement.

Activation et désactivation de commandes

Il est souvent utile de pouvoir désactiver des commandes, d’invalider ensuite leur état et de demander à ce qu’elles soient de nouveau vérifiées pour déterminer si elles peuvent ou non être exécutés. Pour permettre cela, l’attribut RelayCommand expose la propriété CanExecute, qui permet de désigner la propriété ou la méthode cible à utiliser pour déterminer si une commande peut être exécutée :

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

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

De cette façon, la méthode CanGreetUser est appelée lorsque le bouton est lié en premier lieu à l’interface utilisateur (par exemple, à un bouton). EIle est ensuite de nouveau appelée chaque fois que IRelayCommand.NotifyCanExecuteChanged est appelé au niveau de la commande.

Par exemple, voici comment une commande peut être liée à une propriété pour contrôler son état :

[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}"/>

Dans cet exemple, la propriété SelectedUser générée appelle la méthode GreetUserCommand.NotifyCanExecuteChanged() chaque fois que sa valeur change. L’interface utilisateur a un contrôle Button qui se lie à GreetUserCommand, ce qui signifie que chaque fois que son événement CanExecuteChanged est déclenché, il appelle de nouveau sa méthode CanExecute. Il s’ensuit une évaluation de la méthode CanGreetUser enveloppée, qui retourne le nouvel état du bouton qui varie selon que l’instance User d’entrée (qui est liée à la propriété SelectedUser dans l’interface utilisateur) a la valeur null ou non. Cela signifie que chaque fois que SelectedUser change, GreetUserCommand s’active ou non selon que cette propriété a une valeur ou non, ce qui est le comportement souhaité dans ce scénario.

Remarque

La commande n’est pas automatiquement informée du moment auquel la valeur de retour de la propriété ou de la méthode CanExecute a changé. Il incombe au développeur d’appeler IRelayCommand.NotifyCanExecuteChanged pour invalider la commande et demander à ce que la méthode CanExecute liée soit de nouveau évaluée pour ensuite mettre à jour l’état visuel du contrôle lié à la commande.

Gestion des exécutions simultanées

Du moment qu’une commande est asynchrone, il est possible de la configurer pour décider si elle doit ou non autoriser les exécutions simultanées. Lorsque l’attribut RelayCommand est utilisé, il est possible de le définir via la propriété AllowConcurrentExecutions. La valeur par défaut est false, ce qui signifie que tant qu’une exécution n’est pas en attente, la commande signale que son état est désactivé. À l’inverse, si elle est définie sur true, le nombre d’appels simultanés pouvant être mis en file d’attente est illimité.

Notez que si une commande accepte un jeton d’annulation, un jeton est également annulé si une exécution simultanée est demandée. Si les exécutions simultanées sont autorisées, la principale différence vient du fait que la commande reste activée et qu’elle lance une nouvelle exécution demandée sans attendre que la précédente ait bel et bien abouti.

Gestion des exceptions asynchrones

Les commandes de relais asynchrones peuvent gérer les exceptions de deux manières différentes :

  • Attendre et regénérer (par défaut) : lorsque la commande attend la fin d’un appel, les exceptions sont naturellement générées dans le même contexte de synchronisation. Cela signifie généralement que les exceptions générées bloquent tout simplement l’application, ce qui est un comportement en phase avec celui des commandes synchrones (où les exceptions générées bloquent également l’application).
  • Transmettre les exceptions au planificateur de tâches : si une commande est configurée pour transmettre les exceptions au planificateur de tâches, les exceptions générées ne bloquent pas l’application, mais elles deviennent disponibles via le IAsyncRelayCommand.ExecutionTask exposé et remontent vers TaskScheduler.UnobservedTaskException. Si cette option autorise des scénarios plus avancés (par exemple, avec la liaison des composants de l’interface utilisateur à la tâche et l’affichage de résultats différents selon l’issue de l’opération), il est plus difficile de l’utiliser correctement.

Des commandes qui attendent et regénèrent les exceptions correspondent au comportement par défaut. Cela peut se configurer via la propriété FlowExceptionsToTaskScheduler :

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

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

Dans ce cas, try/catch n’est pas nécessaire dans la mesure où les exceptions ne risquent plus de bloquer l’application. De même, notez que les autres exceptions non liées ne sont pas regénérées automatiquement. Vous devez donc bien réfléchir à la façon d’aborder chaque scénario individuel et veiller à configurer le reste du code de manière appropriée.

Commandes d’annulation pour les opérations asynchrones

La dernière option concernant les commandes asynchrones est la possibilité de demander la génération d’une commande d’annulation. Il s’agit d’une interface ICommand enveloppant une commande de relais asynchrone qui permet de demander l’annulation d’une opération. Cette commande signale automatiquement son état pour indiquer si elle peut ou non être utilisée à un moment donné. Par exemple, si la commande liée n’est pas en cours d’exécution, elle signale également son état comme non exécutable. Voici comment elle peut être utilisée :

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

Cette commande entraîne également la génération d’une propriété DoWorkCancelCommand. Celle-ci peut être liée à un autre composant d’interface utilisateur pour permettre aux utilisateurs d’annuler facilement les opérations asynchrones en attente.

Ajout d’attributs personnalisés

Comme pour les propriétés observables, le générateur RelayCommand assure également la prise en charge des attributs personnalisés pour les propriétés générées. Pour en tirer profit, vous pouvez simplement utiliser la cible [property: ] dans les listes d’attributs pour les méthodes annotées ; le kit de ressources MVVM transfère alors ces attributs aux propriétés de commande générées.

Par exemple, prenons le cas de cette méthode :

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

Elle génère une propriété GreetUserCommand avec l’attribut [JsonIgnore]. Vous pouvez utiliser autant de listes d’attributs ciblant la méthode que vous le souhaitez, toutes seront transférées aux propriétés générées.

Exemples

  • Consultez l’exemple d’application (pour plusieurs infrastructures d’interface utilisateur) pour voir le kit d’outils MVVM à l’œuvre.
  • Vous trouverez également d’autres exemples dans les tests unitaires.