【问题标题】:Is wrong to invoke an async method from property setter in WPF view model?从 WPF 视图模型中的属性设置器调用异步方法是错误的吗?
【发布时间】:2018-05-04 13:06:42
【问题描述】:

当属性更改其值时,我想调用从 Web 服务获取数据的异步方法,然后更新 UI 绑定到的另一个属性,从而导致 UI 更新。更新是异步的对我来说很有意义,因为我希望 UI 在更新进行时保持响应。

从非异步设置器调用异步方法是否错误?我注意到如果异步方法返回 void 则 VS 不会抱怨,但如果它返回 Task 则 Visual Studio 抱怨调用未等待。我的代码如下所示:

public int Property1
{
    set 
    {
        _property1 = value;
        NotityPropertyChanged();
        UpdateData();
    }
}

private async void UpdateData()
{
    // show data loading message
    var data = await GetDataFromWebService();
    Property2 = data;
    // hide data loading message
}

这似乎有效,但我想知道如果返回类型是任务,我是否没有按照预期的方式使用异步,因为我从 VS 收到警告。

更新:一些答案和 cmets 建议用户使用命令而不是更新以响应属性的变化。对于我的情况,我不确定如何应用它,因此我提供了有关 UI 预期如何工作的更多详细信息。

在用户界面中有日期选择器(绑定到视图模型上的相关属性),用户可以在其中选择他想要查看记录的日期。当用户选择新日期时,应用程序应显示忙碌指示符,然后在后台获取记录以避免阻塞 UI 线程。最好我希望在选择日期时启动更新,而不需要用户在选择日期后按下按钮。

最好将日期选择器的 SelectionChanged 事件绑定到 ViewModel 上的异步命令,或者为 SelectionChanged 设置一个同步处理程序,直接调用视图模型上的更新方法?

【问题讨论】:

  • 属性不应该是async,既不是getter也不是setter。
  • 您在此处使用 VM 属性设置器,就好像它是模型操作、命令或按钮单击处理程序一样。 click 应该调用方法或操作,而不是设置属性并期望间接操作。
  • @Panagiotis Kanavos - 我的 UI 需要更新以响应日期字段的更改而不是单击按钮 - 已按问题更新以使这一点更加清晰。您能否建议如何在不启动 setter 更新的情况下适应该场景?一个答案建议收听 PropertyChanged 事件,但由于该事件是从每个属性设置器调用的,我认为这与直接从属性设置器启动更新没有太大区别。

标签: c# wpf task-parallel-library


【解决方案1】:

从非 async setter 调用 async 方法有错吗?

简而言之,是的。属性不应在其设置器中启动异步后台操作。

我建议您阅读关于该主题的 Stephen Cleary 的博文和 MSDN 文章:

异步编程:异步 MVVM 应用程序的模式:数据绑定: https://msdn.microsoft.com/en-us/magazine/dn605875.aspx

异步 OOP 3:属性: https://blog.stephencleary.com/2013/01/async-oop-3-properties.html

您可能还想研究一个功能性 MVVM 框架,例如 ReactiveUI,它通过将属性转换为您可以订阅的可观察值流来处理这种情况:https://reactiveui.net/docs/getting-started/

【讨论】:

  • 这里不需要 ReactiveUI。这不是关于观察属性的变化,而是使用一个输入参数执行单个长时间运行的操作并计算另一个,例如使用用户输入的 ID 加载客户记录。这就是普通的旧方法或命令的工作。当这些完成后,他们应该更新虚拟机或创建一个新虚拟机
【解决方案2】:

这不是已经说过的“最干净”的方式,但你可以离开它。首先它并没有什么害处,除非您在快速分配后面隐藏了一个意想不到的漫长而昂贵的操作。

MSDN says:

异步方法也可以有一个 void 返回类型。此返回类型主要用于定义需要 void 返回类型的事件处理程序。异步事件处理程序通常作为异步程序的起点。

不能等待具有 void 返回类型的异步方法,并且 返回 void 的方法的调用者无法捕获任何异常 方法抛出。

一个简单的解决方案是监听 PropertyChanged 事件。这更像是逃避二传手的工作。更好的方法是通过向其添加异步执行来实现 ICommand 接口异步,从而使您的 ViewModel 公开更新命令。然后,只要属性更改,您就可以从您的视图中调用此 AsyncCommand。您还可以将新值作为命令参数传递给 ViewModel。

【讨论】:

  • 相反,MSDN 警告async void 是用于事件处理程序的。你引用的 sn-p 就是这么说的。属性不是事件处理程序。在其他任何地方使用async void 是一个严重的错误。使用 properties 来启动工作也是一个糟糕的设计。
  • 是的,他们暗示它说调用者没有得到任何等待作为回报,并且不能捕获任何异常。你可以知道。在某种方式无法正常工作的情况下,这不是“错误”,您的事件处理程序会这样做。但不是最佳实践。 docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
  • 不,大错特错。这是一种非常糟糕的做法,您甚至会收到代码分析器的警告。您发布的链接并没有说出您的想法 - 当他们说“主要”时,他们的意思是“没有非常好的理由不做任何其他事情”。 演练指南明确规定这样做。
  • 您会发现 很多 的 SO 问题,人们使用 async void 会被烧死。在几乎所有情况下,他们都没有意识到can't be awaited 意味着应用程序可能会在async void 方法甚至运行之前终止,或者即使稍后调用该方法也可以在该方法之前运行一些其他代码.该方法的本地数据可以在完成之前被垃圾收集
  • 并且在async void 中生成的服务代理可以在对 Web 服务的调用完成之前进行垃圾收集
【解决方案3】:

仅在属性发生更改时自动调用更新本身通常是个坏主意。
通常一个按钮和一个命令是一个更好的计划。 您也可以从 UI 调用命令,而不是使用该设置器。这会相对简单。

至少,您需要一个不同的线程来以这种方式调用某些 Web 服务。
一个火而忘记的线程并不可怕。
只要你不在乎忘记之后会发生什么。

【讨论】:

    猜你喜欢
    • 2018-06-21
    • 1970-01-01
    • 2022-01-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-17
    • 1970-01-01
    • 2017-11-11
    相关资源
    最近更新 更多