Je veux lier la méthode asynchrone à une commande déléguée dans le cadre de prisme dans Xamarin.Forms et ma question est de savoir comment le faire?
La solution ci-dessous est-elle correcte? Existe-t-il un piège? (blocage, interface utilisateur lente ou gelée, mauvaises pratiques, ...)
{ // My view model constructor
...
MyCommand = new DelegateCommand(async () => await MyJobAsync());
...
}
private async Task MyJobAsync()
{
... // Some await calls
... // Some UI element changed such as binded Observable collections
}
Vous pouvez utiliser async void
directement. Cependant, quelques notes de mon expérience ...
La structure de votre code est la suivante: démarrer le fonctionnement asynchrone, puis mettre à jour l'interface utilisateur avec les résultats. Cela implique pour moi que vous seriez mieux servi avec un NotifyTask<T>
type d'approche de la liaison de données asynchrone , pas des commandes . Voir mon article de liaison de données async MVVM pour en savoir plus sur la conception derrière NotifyTask<T>
(mais notez que le dernier code a un bug corrigé et d'autres améliorations).
Si vous avez vraiment besoin d'une commande asynchrone (ce qui est beaucoup plus rare) ), vous pouvez utiliser async void
directement ou créez un type de commande async comme je le décris dans mon article sur commandes async MVVM . J'ai aussi types pour supporter cela mais les API pour ceux-ci sont plus en flux.
Si vous choisissez d'utiliser async void
directement:
async Task
logique publique, ou du moins accessible à vos tests unitaires.DelegateTask
, toutes les exceptions de votre délégué doivent être correctement gérées.Comme cela a déjà été mentionné, la façon de gérer le code asynchrone avec la commande déléguée est d'utiliser async void
. Il y a eu beaucoup de discussions à ce sujet, bien au-delà des formes Prism ou Xamarin. L'essentiel est que ICommand
que les deux Xamarin Forms Command
et Prism DelegateCommand
sont limités par ICommand
's void Execute(object obj)
. Si vous souhaitez obtenir plus d'informations à ce sujet, je vous encourage à lire le blog de Brian Lagunas expliquant pourquoi le gestionnaire DelegateCommand.FromAsync
Est obsolète .
Généralement, la plupart des problèmes sont traités très facilement en mettant à jour le code. Par exemple. J'entends souvent des plaintes à propos de Exceptions
comme "la raison" pour laquelle FromAsync était nécessaire, seulement pour voir dans leur code qu'ils n'avaient jamais eu de prise d'essai. Parce que async void
Est un incendie et oubliez, une autre plainte que j'ai entendue est qu'une commande pourrait s'exécuter deux fois. Cela est également facilement corrigé avec DelegateCommands
ObservesProperty
et ObservesCanExecute
.
Je pense que les deux principaux problèmes lors de l'appel d'une méthode asynchrone à partir d'une qui s'exécute de manière synchrone (ICommand.Execute) sont 1) le refus de s'exécuter à nouveau pendant que l'appel précédent est toujours en cours d'exécution 2) la gestion des exceptions. Les deux peuvent être abordés avec une implémentation comme la suivante (prototype). Ce serait un remplacement asynchrone pour le DelegateCommand.
public sealed class AsyncDelegateCommand : ICommand
{
private readonly Func<object, Task> func;
private readonly Action<Exception> faultHandlerAction;
private int callRunning = 0;
// Pass in the async delegate (which takes an object parameter and returns a Task)
// and a delegate which handles exceptions
public AsyncDelegateCommand(Func<object, Task> func, Action<Exception> faultHandlerAction)
{
this.func = func;
this.faultHandlerAction = faultHandlerAction;
}
public bool CanExecute(object parameter)
{
return callRunning == 0;
}
public void Execute(object parameter)
{
// Replace value of callRunning with 1 if 0, otherwise return - (if already 1).
// This ensures that there is only one running call at a time.
if (Interlocked.CompareExchange(ref callRunning, 1, 0) == 1)
{
return;
}
OnCanExecuteChanged();
func(parameter).ContinueWith((task, _) => ExecuteFinished(task), null, TaskContinuationOptions.ExecuteSynchronously);
}
private void ExecuteFinished(Task task)
{
// Replace value of callRunning with 0
Interlocked.Exchange(ref callRunning, 0);
// Call error handling if task has faulted
if (task.IsFaulted)
{
faultHandlerAction(task.Exception);
}
OnCanExecuteChanged();
}
public event EventHandler CanExecuteChanged;
private void OnCanExecuteChanged()
{
// Raising this event tells for example a button to display itself as "grayed out" while async operation is still running
var handler = CanExecuteChanged;
if (handler != null) handler(this, EventArgs.Empty);
}
}
async void
Personnellement, j'éviterais "async void" à tout prix. Il est impossible de savoir de l'extérieur quand l'opération est terminée et la gestion des erreurs devient délicate. En ce qui concerne ce dernier, par exemple, écrire une méthode "async Task" qui est appelée à partir d'une méthode "async void" doit presque être conscient de la façon dont sa tâche défaillante est propagée:
public async Task SomeLogic()
{
var success = await SomeFurtherLogic();
if (!success)
{
throw new DomainException(..); // Normal thing to do
}
}
Et puis quelqu'un écrit un autre jour:
public async void CommandHandler()
{
await SomeLogic(); // Calling a method. Normal thing to do but can lead to an unobserved Task exception
}
Le thread d'interface utilisateur exécute-t-il DelegateCommand et les threads d'arrière-plan en cours d'exécution attendent-ils l'expression?
Oui, le thread d'interface utilisateur exécute le DelegateCommand
. Dans le cas d'une async
, elle s'exécute jusqu'à la première instruction await
, puis reprend son travail de thread d'interface utilisateur normal. Si l'attente est configurée pour capturer le contexte de synchronisation (c'est-à-dire que vous utilisez pas utilisez .ConfigureAwait(false)
), le thread d'interface utilisateur continuera d'exécuter le DelegateCommand
après le await
.
Le thread d'interface utilisateur exécute-t-il DelegateCommand et les threads d'arrière-plan en cours d'exécution attendent l'expression?
Que "l'expression en attente" s'exécute sur un thread d'arrière-plan, un thread de premier plan, un thread de pool de threads ou tout ce qui dépend de l'API que vous appelez. Par exemple, vous pouvez pousser le travail lié au processeur vers le pool de threads en utilisant Task.Run
ou vous pouvez attendre une opération d'E/S sans utiliser de thread du tout avec des méthodes comme Stream.ReadAsync
Changez votre code dans le constructeur en ceci:
MyCommand = new DelegateCommand(() => { MyJobASync()});
et dans votre méthode:
private async Task MyJobASync()
{
// your method
}