如何从属性更改事件中调用异步方法?

3
我有一个基于MVVM架构的WPF应用程序。我正在我的ViewModels上实现常见且广泛使用的“INotifyPropertyChanged”接口,因为我需要对用户交互做出反应。
但是,如何在同步的PropertyChanged事件处理程序中执行异步操作(例如加载一些数据)而不使用“async void”'hack'呢?
提前感谢!
编辑
我需要避免“async void”的主要原因是因为我在测试驱动环境中工作。异步void方法无法进行测试 :(

2
使用async void与事件处理程序不是一种hack。这是首选的解决方案。事件处理程序本质上是“fire and forget”,因此不应该期望事件处理程序返回任何值。 - FCin
在属性中加载数据通常被认为是不好的实践。这通常是创建方法而不是使用属性的标准之一。 - Crowcoder
4个回答

3

实际上,这与 async void 无关。

通常情况下,您希望触发异步操作并让属性设置器返回。示例代码片段:

private string carManufacturerFilter;

public string СarManufacturerFilter
{
    get { return carManufacturerFilter; }
    set
    {
        if (carManufacturerFilter != value)
        {
            carManufacturerFilter = value;
            OnPropertyChanged();

            // fire async operation and forget about it here;
            // you don't need it to complete right now;
            var _ = RefreshCarsListAsync();
        }
    }
}

private async Task RefreshCarsListAsync()
{
    // call some data service
    var cars = await someDataService.GetCarsAsync(carManufacturerFilter)
        .ConfigureAwait(false);

    // ...
}

注意,这里有很多需要添加的内容:
  • 由于这是一种“发出即忘”的方法,您需要阻止用户输入直到操作完成。换句话说,应该有某种繁忙指示器;
  • 您可能希望延迟异步操作的触发。当存在字符串属性时,通常适用此项。您不希望在用户键入每个字符后都触发异步操作。相反,最好等待用户完成输入;
  • 可能会有几个属性触发同一个异步操作(想象一下复杂的数据过滤器)。其中一些应立即触发操作(例如复选框),而另一些需要延迟触发;
  • 您需要处理异步方法内部的异常,并以某种方式显示错误。

P.S. 我强烈建议您查看Reactive UI


2
它不会抱怨没有等待可等待的方法吗? - FCin
如果你关心编译器警告,那么赋值(var _ = )可以抑制它。在现实生活中,这段代码将被包装成类似于AsyncCommand的东西。这只是一个示例,旨在为OP提供基本思路。这不是生产就绪的代码。 - Dennis
@Dennis 当使用async void时,当任务无法等待时,我应该如何断言任何内容? - ˈvɔlə
这里没有异步空返回。这是一个普通的异步方法,返回Task。但无论如何,您都不应该等待它完成,因为这会导致UI冻结。 - Dennis
在我看来,这是一个代码异味的原因有两个。 1)你没有等待任务完成。 2)你通过将任务赋值给空变量来“人为地”消除编译器关于未等待任务的警告。 - undefined
显示剩余5条评论

2
< p > async void 能够被支持是为了允许在通常为void的事件处理程序中使用await。如果你想要进行测试,可以将整个代码编写在另一个async Task方法中,并直接调用事件处理程序。在测试中测试这个方法。


"Original Answer"翻译成"最初的回答"
void OnPropertyChanged(PropertyChangedEventArgs e)
{
    OnPropertyChangedAsync(e)
}

// Test this method
async Task OnPropertyChangedAsync(PropertyChangedEventArgs e)
{
    ...
}

1

虽然有点晚了,但我想说一下。首先,注意到INotifyPropertyChanged表示你的视图模型将实现该接口。

public event PropertyChangedEventHandler? PropertyChanged;

其次,如果您检查PropertyChangedEventHandler,您会发现它是一个委托
public delegate void PropertyChangedEventHandler(object? sender, PropertyChangedEventArgs e);

为什么不使用 Func<> 实现自己的委托呢?例如,如果你想坚持使用 (object? sender, PropertyChangedEventArgs e) 参数模式,那么可以实现类似以下的内容:

public Func<object?, PropertyChangedEventArgs, Task>? PropertyChangedAsync { get; set; }

在同一位置(或替代)public event PropertyChangedEventHandler? PropertyChanged;

从那里,您可以像处理PropertyChanged一样“连接它”:

ViewModel.PropertyChanged += OnViewModelPropertyChanged;    
ViewModel.PropertyChangedAsync += OnViewModelPropertyChangedAsync;

private void OnViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    // Do something (synchronously) with the notification
}

private Task OnViewModelPropertyChangedAsync(object? sender, PropertyChangedEventArgs e)
{
    // Do something (asynchronously) with the notification
    return Task.CompletedTask;
}

顺便说一下,如果你想要更高级的操作,你也可以创建一个自定义的 PropertyChangedEventHandler 事件,其接口与 INotifyPropertyChanged 非常相似。显然,唯一的区别是你需要返回 Task 或者 Task<T> 而不是 void。我上面提到的使用 Func<object?, PropertyChangedEventArgs, Task> 的解决方案只是更加简洁(只有一行代码)。


0

不行。 INotifyPropertyChanged 不支持异步调用。你需要进行一些黑科技,或者重新考虑你的策略。

INotifyPropertyChanged 不适用于异步操作。它的目标是使一个类通知 UI 其数据已更改。UI 在专用线程中工作,因此必须避免跨线程操作。

你应该使用“可怕的”async void方法。

你也可以使用 Dispatcher.BeingInvoke (async () => { … await …} ),但这与使用 async void 相同。


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