如何在视图模型中调用异步命令?

12

我有这段代码,我想把它移到视图模型中:

resetButton.Clicked += async (sender, e) =>
{
   if (App.totalPhrasePoints < 100 || await App.phrasesPage.DisplayAlert(
                "Reset score",
                "You have " + App.totalPhrasePoints.ToString() + " points. Reset to 0 ? ", "Yes", "No"))
      App.DB.ResetPointsForSelectedPhrase(App.cfs);
};

我意识到我需要设置类似这样的东西:

在我的XAML代码中;

<Button x:Name="resetButton" Text="Reset All Points to Zero" Command="{Binding ResetButtonClickedCommand}"/>

而在我的 C# 代码中:

private ICommand resetButtonClickedCommand;

public ICommand ResetButtonClickedCommand
{
   get
   {
      return resetButtonClickedCommand ??
      (resetButtonClickedCommand = new Command(() =>
      {

      }));

    }

但是我该如何将异步操作嵌入到命令中呢?

5个回答

27

你可以尝试像这样做:

(resetButtonClickedCommand = new Command(async () => await SomeMethod()));

async Task SomeMethod()
{
    // do stuff
}

如果使用为此设计的子类命令,则代码会稍微简单一些:resetButtonClickedCommand = new AsyncCommand(SomeMethod); - ToolmakerSteve
@Neil - 抱歉,是我的错误。我以为它是ICommand的内置子类,但我发现它在我们公司的代码库中。我添加了一个定义class AsyncCommand的答案 - ToolmakerSteve

11

补充之前的回答,如果需要向命令传递参数,可以使用类似以下的方式:

(resetButtonClickedCommand = new Command<object>(async (o) => await SomeMethod(o)));

async Task SomeMethod(object o)
{
    // do stuff with received object
}
你可以将上面的object替换为任何你想要的东西。

3
进一步测试后,对于大多数用途来说,这个类可能过于复杂了。
尽管有些人投了反对票,chawala的回答在我的测试中表现良好。
重要的是,方法声明中存在async就足以避免阻塞UI线程。因此,在我看来,chawala的答案“没有问题”;不值得那些反对票。
明确一点:显式的async => await回答当然完全没有问题,没有任何问题。如果这样做可以让你更加自信,请使用它们。
我的回答旨在使调用站点更加清洁。然而,maxc的第一个评论是正确的:我所做的不再与显式的async => await“完全相同”。到目前为止,我还没有发现任何情况会有影响。无论是否在new Command中使用async/await,在单击按钮多次快速时,所有单击都会排队。我甚至测试了SomeMethod切换到新页面。我尚未发现任何与显式的async/await有任何区别。在我的测试中,此页面上的所有答案都具有相同的结果。
如果您不使用Task结果,并且没有添加任何代码来处理此方法期间发生的任何异常,那么async void与async Task一样有效。
在这个类代码中,请参见我的评论“待定:考虑在此处添加异常处理逻辑”。
换句话说,大多数开发人员编写的代码并没有什么区别。如果这是个问题,那么在他们的new Command(await () => async SomeMethod());版本中同样会出现问题。
下面是一个方便的类。使用它可以简化将命令与async组合的过程。
如果您有一个像这样的async方法(从已接受的答案中复制):
async Task SomeMethod()
{
    // do stuff
}

没有这个类,使用Command中的async方法将会像这样(来自已接受的答案):
resetButtonClickedCommand = new Command(async () => await SomeMethod());

使用类可以使代码更加简洁:

resetButtonClickedCommand = new AsyncCommand(SomeMethod);

结果等同于没有使用这个类时显示的略长的代码行。虽然没有太大的好处,但有一个隐藏杂乱无章的代码和给一个经常使用的概念命名的代码是很好的。

如果有一个带参数的方法,那么这个好处会更加明显:

async Task SomeMethod(object param)
{
    // do stuff
}

没有类:

yourCommand = new Command(async (param) => await SomeMethod(param));

使用类(与无参数情况相同;编译器调用适当的构造函数):

yourCommand = new AsyncCommand(SomeMethod);

class AsyncCommand 的定义:

using System;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Windows.Input;

namespace MyUtilities
{
    /// <summary>
    /// Simplifies using an "async" method as the implementor of a Command.
    /// Given "async Task SomeMethod() { ... }", replaces "yourCommand = new Command(async () => await SomeMethod());"
    /// with "yourCommand = new AsyncCommand(SomeMethod);".
    /// Also works for methods that take a parameter: Given "async Task SomeMethod(object param) { ... }",
    /// Usage: "yourCommand = new Command(async (param) => await SomeMethod(param));" again becomes "yourCommand = new AsyncCommand(SomeMethod);".
    /// </summary>
    public class AsyncCommand : ICommand
    {
        Func<object, Task> _execute;
        Func<object, bool> _canExecute;

        /// <summary>
        /// Use this constructor for commands that have a command parameter.
        /// </summary>
        /// <param name="execute"></param>
        /// <param name="canExecute"></param>
        /// <param name="notificationSource"></param>
        public AsyncCommand(Func<object,Task> execute, Func<object, bool> canExecute = null, INotifyPropertyChanged notificationSource = null)
        {
            _execute = execute;
            _canExecute = canExecute ?? (_ => true);
            if (notificationSource != null) 
            {
                notificationSource.PropertyChanged += (s, e) => RaiseCanExecuteChanged();   
            }
        }

        /// <summary>
        /// Use this constructor for commands that don't have a command parameter.
        /// </summary>
        public AsyncCommand(Func<Task> execute, Func<bool> canExecute = null, INotifyPropertyChanged notificationSource = null)
            :this(_ => execute.Invoke(), _ => (canExecute ?? (() => true)).Invoke(), notificationSource)
        {
        }

        public bool CanExecute(object param = null) => _canExecute.Invoke(param);

        public Task ExecuteAsync(object param = null) => _execute.Invoke(param);

        public async void Execute(object param = null)
        {
            // TBD: Consider adding exception-handling logic here.
            // Without such logic, quoting https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming
            // "With async void methods, there is no Task object, so any exceptions thrown out of an async void method will be raised directly on the SynchronizationContext that was active when the async void method started."
            await ExecuteAsync(param);
        }

        public event EventHandler CanExecuteChanged;

        public void RaiseCanExecuteChanged()
        {
            CanExecuteChanged?.Invoke(this, EventArgs.Empty);
        }
    }

}

关于下面对async void Execute的评论。无论是class Command还是interface ICommand都有void Execute方法。与这些兼容意味着具有相同的方法签名-因此通常推荐的async Task MethodName()在这里不可行。请参见我的评论中的链接,以了解在此处使用void的影响。

这个问题中最不受欢迎的答案是建议仅使用async void而不是async Task。而你的答案本质上就是那个,只是多了一些步骤。它将我的操作包装在你的类中,然后在async void方法(async void Execute)中执行它。那么有什么区别呢?直接在第一次使操作成为async void并使用new Command(SomeAction)不是更容易吗? - maxc137
@МаксимКошевой - 这是一个很好的问题。 我不太了解,但我的理解是:如果该方法在事件处理程序中使用,则可以这样做。 我采取保守的方法,让该方法返回一个任务(Task),以防它被用于其他地方。 在最佳实践指南中,关于“避免异步空返回类型”的指导,“异常”是“事件处理程序”。 - ToolmakerSteve
@МаксимКошевой - 另外,这里可能有相关的SO讨论。阅读后,我意识到我的答案是不完整的:在await ExecuteAsync(param);期间发生的异常没有处理逻辑。在该调用周围/之后添加此类逻辑可以使程序员有机会对异常进行不同的处理。只需注意(来自该指南):“异步void方法抛出的异常将直接在异步void方法启动时处于活动状态的同步上下文上引发。” - ToolmakerSteve
@maxc137 - 经过进一步测试,我没有发现chawala的被downvote的答案会导致任何问题。在我看来,XAML命令处理程序本质上是“fire-and-forget”,因此缺少async/await与Command(async () => await ...似乎对我来说无关紧要。如果有任何相反的证据,我会非常感兴趣。底线:我的AsyncCommand类似乎是不必要的。chawala的答案似乎完全可行。async void确保UI不被阻塞,这才是最重要的。 - ToolmakerSteve

1
为了使用参数实例化AsyncCommand,正确的方法是这样的:
this.SaveCommand = new AsyncCommand((o) => SaveCommandHandlerAsync (o));

或者需要

如果参数是类型为“object”,编译器将自动为您执行此操作。因此,给定声明Task SaveCommandHandlerAsync(object someParamName) { ... },更简单的用法new AsyncCommand(SaveCommandHandlerAsync);也可以工作。注意:对于其他人阅读此内容:AsyncCommand不是内置框架类。要么使用定义它的某个MVVM框架,要么编写自己的框架(如我的答案)。 - ToolmakerSteve
Xamarin Forms 中没有 AsyncCommand。 - Ângelo Polotto
1
@ÂngeloPolotto - 你是对的;我最初错误地建议它是内置的。请查看我的答案以获取实现方法:https://dev59.com/u1YO5IYBdhLWcg3wP_Qb#63514672。 - ToolmakerSteve

-3

你也可以这样写:

(resetButtonClickedCommand = new Command(DoSomething));

async void DoSomething()
{
    // do something
}

注意:SomeMethod 函数显示警告。


尽管有人对此投票反对,但我还没有发现任何会导致问题的情况。有趣的是,最新的C#编译器不再在 new Command(DoSomething))处给出警告。请参见我的答案 上评论中的链接,以讨论“async void”的问题。缺少 awaitCommand(async () => await ...) 的唯一后果是,在 DoSomething 完成之前,Command 就已经返回了。对于命令处理程序而言,这是无关紧要的,就我所知。在XAML中使用命令本质上是一个“fire-and-forget”操作。 - ToolmakerSteve

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接