【问题标题】:How can I call an async command with a view model?如何使用视图模型调用异步命令?
【发布时间】:2021-10-28 11:14:36
【问题描述】:

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

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(() =>
      {

      }));

    }

但是我怎样才能将异步操作放入命令中呢?

【问题讨论】:

    标签: xamarin xamarin.forms


    【解决方案1】:

    你可以试试这样的:

    (resetButtonClickedCommand = new Command(async () => await SomeMethod()));
    
    async Task SomeMethod()
    {
        // do stuff
    }
    

    【讨论】:

    • 如果使用为此设计的命令子类,代码会稍微简单一些:resetButtonClickedCommand = new AsyncCommand(SomeMethod);
    • @Neil - 抱歉,我的错误。我以为它是ICommand 的内置子类,但我看到它在我公司的代码库中。 I have added an answer that defines class AsyncCommand.
    【解决方案2】:

    为了扩展已经提供的答案,如果您需要将参数传递给命令,您可以使用类似

    (resetButtonClickedCommand = new Command<object>(async (o) => await SomeMethod(o)));
    
    async Task SomeMethod(object o)
    {
        // do stuff with received object
    }
    

    你也可以用你想要的任何东西替换上面的object

    【讨论】:

      【解决方案3】:

      在进一步的测试中,这个类对于大多数用途来说可能是多余的。

      尽管投了反对票,chawala's answer 在我的测试中运行良好。

      重要的是,方法声明中async 的存在足以避免阻塞UI 线程。因此,chawala 的回答是“未破”;不值得那些反对票,恕我直言。

      要明确:明确的async =&gt; await 答案当然很好,没有任何问题。如果这能让您更有信心,请使用它们。

      我的回答旨在使呼叫站点更清洁。 但是,maxc 的第一条评论是正确的:我所做的与明确的async =&gt; await 不再“相同”。 到目前为止,我还没有发现任何重要的情况。无论new Command 内是否有async/await,如果您快速点击一个按钮几次,所有点击都会排队。我什至用SomeMethod 切换到新页面进行了测试。我还没有发现与明确的async/await 有任何区别。 在我的测试中,此页面上的所有答案都有相同的结果。

      async voidasync Task 一样有效,如果你没有使用 Task 结果,并且你没有添加任何代码来做一些有用的事情,但有任何例外在此方法期间发生的。

      在此类代码中,请参阅我的评论“待定:考虑在此处添加异常处理逻辑。”。

      或者换一种说法:大多数开发人员都在编写没有任何区别的代码。如果这是一个问题,那么在他们的new Command(await () =&gt; 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://docs.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 Commandinterface ICommand 都有方法 void Execute。与这些兼容意味着具有相同的方法签名 - 因此通常推荐的 async Task MethodName() 不是这里的选项。请参阅我的 cmets 中的链接,了解在此处使用 void 的含义。

      【讨论】:

      • 这个问题中最不喜欢的答案是建议只使用 async void 而不是 async Task。你的答案本质上就是那个,但有额外的步骤。它将我的动作包装在你的类中,然后在 async void 方法(async void Execute)中执行它。那么有什么区别呢?一开始就让动作 async void 并使用 new Command(SomeAction) 会更容易吗
      • @МаксимКошевой - 这是一个很好的问题。我不完美的理解是:如果该方法在事件处理程序中使用,那么可以这样做。我采取了让该方法返回一个任务的保守方法,以防万一它在其他地方使用。在best practices guide图 1 异步编程指南摘要关于“避免异步无效”的指导,我们看到异常“事件处理程序”。
      • @МаксимКошевой - 也可能相关SO discussion here。读到这里,我意识到我的答案是不完整:没有逻辑可以处理await ExecuteAsync(param); 期间发生的异常。在调用前后添加的这种逻辑使程序员有机会对异常做一些不同的事情。请注意(来自该指南):“...从异步 void 方法抛出的异常将直接在异步 void 方法启动时处于活动状态的 SynchronizationContext 上引发。”
      • @maxc137 - 在进一步测试中,我没有发现任何chawala's downvoted answer 导致问题的情况。在我看来,XAML 命令处理程序本质上是“即发即弃”,因此缺乏 async/await 与 Command(async () =&gt; await ... 对我来说似乎没有意义。我会对任何相反的证据非常感兴趣。底线:我的 AsyncCommand 类似乎没有必要。 chawala 的回答似乎完全可行。 async void 确实确保 UI 不被阻塞,这在这里很重要。
      【解决方案4】:

      使用参数实例化 AsyncCommand,这种方法是正确的:

      this.SaveCommand = new AsyncCommand((o) => SaveCommandHandlerAsync (o));
      

      或者是需要

      【讨论】:

      • 如果参数类型为object,编译器将自动为您执行此操作。所以给定声明Task SaveCommandHandlerAsync(object someParamName) { ... },更简单的用法new AsyncCommand(SaveCommandHandlerAsync); 也可以。其他阅读本文的人请注意:AsyncCommand 不是内置框架类。要么使用一些定义它的 MVVM 框架,要么编写你自己的(如我的回答)。
      • Xamarin 表单中没有 AsyncCommand。
      • @ÂngeloPolotto - 你是对的;我是第一个错误地建议它内置的人。请see my answer 实现。
      【解决方案5】:

      你也可以这样写:-

      (resetButtonClickedCommand = new Command(DoSomething));
      
      async void DoSomething()
      {
          // do something
      }
      

      注意:- 它在 SomeMethod 处显示警告。

      【讨论】:

      • 尽管投了反对票,但我没有发现任何会导致问题的情况。有趣的是,最新的 C# 编译器不再在 new Command(DoSomething)) 处发出警告。有关async void 的讨论,请参阅my answer 上我的cmets 上的链接。缺少awaitCommand(async () =&gt; await ...) 的唯一后果是CommandDoSomething 完成之前返回。对于命令处理程序,这无关紧要,AFAIK。 XAML 的命令使用本质上是一种“即发即弃”操作。
      猜你喜欢
      • 2013-06-15
      • 1970-01-01
      • 1970-01-01
      • 2012-06-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多