Attributo RelayCommand

Il RelayCommand tipo è un attributo che consente di generare proprietà dei comandi di inoltro per i metodi con annotazioni. Il suo scopo è eliminare completamente il boilerplate necessario per definire i comandi che eseguono il wrapping di metodi privati in un modello di visualizzazione.

Note

Per funzionare, i metodi con annotazioni devono trovarsi in una classe parziale. Se il tipo è annidato, anche tutti i tipi nell'albero della sintassi della dichiarazione devono essere annotati come parziali. In caso contrario, si verificano errori di compilazione, perché il generatore non sarà in grado di generare una dichiarazione parziale diversa di tale tipo con il comando richiesto.

API della piattaforma:RelayCommand, ICommand, IRelayCommand, IRelayCommand<T>, IAsyncRelayCommand, IAsyncRelayCommand<T>, TaskCancellationToken

Come funziona

L'attributo RelayCommand può essere usato per annotare un metodo in un tipo parziale, ad esempio:

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

Verrà generato un comando simile al seguente:

private RelayCommand? greetUserCommand;

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

Note

Il nome del comando generato verrà creato in base al nome del metodo. Il generatore userà il nome del metodo e aggiungerà "Command" alla fine, e rimuoverà il prefisso "On", se presente. Inoltre, per i metodi asincroni, anche il suffisso "Async" viene rimosso prima che venga aggiunto "Command".

Parametri del comando

L'attributo [RelayCommand] supporta la creazione di comandi per i metodi con un parametro . In tal caso, modificherà automaticamente il comando generato in un IRelayCommand<T>, che accetta un parametro dello stesso tipo:

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

Verrà generato il codice seguente:

private RelayCommand<User>? greetUserCommand;

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

Il comando risultante userà automaticamente il tipo dell'argomento come argomento di tipo.

Comandi asincroni

Il [RelayCommand] comando supporta anche il wrapping di metodi asincroni tramite le IAsyncRelayCommand interfacce e IAsyncRelayCommand<T> . Questo viene gestito automaticamente ogni volta che un metodo restituisce un Task tipo. Per esempio:

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

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

Ciò comporterà il codice seguente:

private AsyncRelayCommand? greetUserCommand;

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

Se il metodo accetta un parametro, anche il comando risultante sarà generico.

Esiste un caso speciale in cui il metodo include un CancellationToken, poiché questo sarà propagato al comando per consentire l'annullamento. Ovvero, un metodo simile al seguente:

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

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

Farà sì che il comando generato passi un token al metodo incapsulato. In questo modo i consumer possono semplicemente chiamare IAsyncRelayCommand.Cancel per segnalare il token e consentire l'arresto corretto delle operazioni in sospeso.

Abilitazione e disabilitazione dei comandi

Spesso è utile poter disabilitare i comandi e successivamente invalidarne lo stato, facendo in modo che verifichino nuovamente se possono essere eseguiti oppure no. Per supportare questa operazione, l'attributo RelayCommand espone la CanExecute proprietà , che può essere usata per indicare una proprietà o un metodo di destinazione da utilizzare per valutare se è possibile eseguire un comando:

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

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

In questo modo, CanGreetUser viene richiamato quando il pulsante viene prima associato all'interfaccia utente ,ad esempio a un pulsante, e quindi viene richiamato di nuovo ogni volta IRelayCommand.NotifyCanExecuteChanged che viene richiamato sul comando.

Ad esempio, questo è il modo in cui un comando può essere associato a una proprietà per controllarne lo stato:

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

In questo esempio, la proprietà generata SelectedUser richiamerà GreetUserCommand.NotifyCanExecuteChanged() il metodo ogni volta che il valore cambia. L'interfaccia utente ha un controllo Button associato a GreetUserCommand, il che significa che ogni volta che viene attivato l'evento CanExecuteChanged, richiamerà il relativo metodo CanExecute. In questo modo verrà valutato il metodo wrappato CanGreetUser, che restituirà il nuovo stato del pulsante a seconda che l'istanza di input User (che nell'interfaccia utente è associata alla proprietà SelectedUser) sia null o meno. Ciò significa che ogni volta SelectedUser che viene modificato, GreetUserCommand diventerà abilitato o meno in base al fatto che tale proprietà abbia un valore, ovvero il comportamento desiderato in questo scenario.

Note

Il comando non sarà automaticamente a conoscenza del momento in cui il valore restituito per il metodo o la CanExecute proprietà è stato modificato. Spetta allo sviluppatore chiamare IRelayCommand.NotifyCanExecuteChanged per invalidare il comando e richiedere la valutazione del metodo collegato CanExecute per aggiornare quindi lo stato di visualizzazione del controllo associato al comando.

Gestione delle esecuzioni simultanee

Ogni volta che un comando è asincrono, può essere configurato per decidere se consentire o meno esecuzioni simultanee. Quando si usa l'attributo RelayCommand , questa proprietà può essere impostata tramite la AllowConcurrentExecutions proprietà . Il valore predefinito è false, vale a dire che fino a quando un'esecuzione non è in sospeso, il comando segnalerà lo stato come disabilitato. Se invece è impostato su true, è possibile accodare qualsiasi numero di chiamate simultanee.

Si noti che se un comando accetta un token di annullamento, verrà annullato anche un token se viene richiesta un'esecuzione simultanea. La differenza principale è che se sono consentite esecuzioni simultanee, il comando rimarrà abilitato e avvierà una nuova esecuzione richiesta senza attendere il completamento effettivo di quello precedente.

Gestione delle eccezioni asincrone

Esistono due diversi modi in cui i comandi di inoltro asincrono gestiscono le eccezioni:

  • Await e rethrow (impostazione predefinita): quando il comando attende il completamento di una chiamata, tutte le eccezioni verranno generate naturalmente nello stesso contesto di sincronizzazione. Ciò significa in genere che le eccezioni sollevate causeranno semplicemente l'arresto anomalo dell'app, un comportamento coerente con quello dei comandi sincroni (in cui anche le eccezioni sollevate causeranno l'arresto anomalo dell'app).
  • Propagare le eccezioni all'utilità di pianificazione: se un comando è configurato per propagare le eccezioni all'utilità di pianificazione, le eccezioni generate non manderanno in arresto anomalo l'app, ma saranno disponibili sia tramite l'oggetto esposto IAsyncRelayCommand.ExecutionTask sia risalendo fino a TaskScheduler.UnobservedTaskException. Ciò consente scenari più avanzati, ad esempio la presenza di componenti dell'interfaccia utente associati all'attività e la visualizzazione di risultati diversi in base al risultato dell'operazione, ma è più complesso usare correttamente.

Il comportamento predefinito prevede che i comandi attendano e rilancino le eccezioni. Questa operazione può essere configurata tramite la FlowExceptionsToTaskScheduler proprietà :

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

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

In questo caso, try/catch non è necessario, perché le eccezioni non provocheranno più arresti anomali dell'app. Si noti che anche questo farà sì che altre eccezioni non correlate non vengano rilanciate automaticamente, pertanto è consigliabile decidere attentamente come affrontare ogni singolo scenario e configurare opportunamente il resto del codice.

Annullare i comandi per le operazioni asincrone

Un'ultima opzione per i comandi asincroni è la possibilità di richiedere la generazione di un comando cancel. Si tratta di un ICommand wrapper che incapsula un comando relay asincrono che può essere utilizzato per richiedere l'annullamento di un'operazione. Questo comando segnalerà automaticamente lo stato in modo da riflettere se può essere usato o meno in un determinato momento. Ad esempio, se il comando collegato non è in esecuzione, segnala lo stato come anche non eseguibile. Questa operazione può essere usata come segue:

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

In questo modo verrà generata anche una DoWorkCancelCommand proprietà . Questo può quindi essere associato ad altri componenti dell'interfaccia utente per consentire agli utenti di annullare facilmente le operazioni asincrone in sospeso.

Aggiunta di attributi personalizzati

Analogamente alle proprietà osservabili, il RelayCommand generatore include anche il supporto per gli attributi personalizzati per le proprietà generate. Per sfruttare questa possibilità, è sufficiente usare il target [property: ] negli elenchi di attributi dei metodi annotati e MVVM Toolkit inoltrerà tali attributi alle proprietà di comando generate.

Si consideri, ad esempio, un metodo simile al seguente:

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

Verrà generata una GreetUserCommand proprietà con l'attributo [JsonIgnore] su di esso. È possibile usare tutti gli elenchi di attributi destinati al metodo desiderato e tutti verranno inoltrati alle proprietà generate.

Esempi

  • Vedere l'app di esempio (per più framework dell'interfaccia utente) per vedere MVVM Toolkit in azione.
  • È anche possibile trovare altri esempi negli unit test.