【问题标题】:How to call an async method from a property setter如何从属性设置器调用异步方法
【发布时间】:2018-06-21 03:11:38
【问题描述】:

这是我的问题:
我在属性过滤器上绑定了一个 WPF 文本框。它用作过滤器:每次 TextBox.Text 更改时,都会设置 Filter 属性。

<TextBox Text="{Binding Filter, UpdateSourceTrigger=PropertyChanged, Mode=OneWayToSource}" />

现在在 ViewModel 上有我的 Filter 属性:每次过滤器更改时,我都会更新我的值。

private string _filter;
public string Filter
{
    get { return _filter; }
    set
    {
        _filter = value;
        // call to an async WEB API to get values from the filter
        var values = await GetValuesFromWebApi(_filter);
        DisplayValues(values);
    }
}

public async Task<string> GetValuesFromWebApi(string query)
{
    var url = $"http://localhost:57157/api/v1/test/result/{query}";
    // this code doesn't work because it is not async
    // return await _httpClient.GetAsync(url).Result.Content.ReadAsStringAsync();
    // better use it this way
    var responseMessage = await _httpClient.GetAsync(url);
    if (responseMessage.IsSuccessStatusCode)
    {
        return await responseMessage.Content.ReadAsStringAsync();
    }
    else
    {
        return await Task.FromResult($"{responseMessage.StatusCode}: {responseMessage.ReasonPhrase}");
    }
}

由于不允许使用异步属性,如果我的绑定需要调用异步方法,我该怎么做?

【问题讨论】:

  • Async property in c#的可能重复
  • 重新排列你的代码,这样你就不需要从你的属性设置器中调用异步方法...
  • 如果你在一个函数中做了很多事情,尤其是可能出错的事情,重新考虑你的设计,让它成为一个属性。让它成为一个函数。

标签: wpf binding async-await


【解决方案1】:

我假设 DisplayValues 方法实现正在更改绑定到 UI 的属性,为了演示,我假设它是 List&lt;string&gt;

private List<string> _values;

public List<string> Values
{
    get
    {  
        return _values;
    }
    private set 
    {
        _values = value;
        OnPropertyChange();
    }
}

它的绑定:

<ListBox ItemsSource="{Binding Values}"/>

现在正如您所说,不允许使属性设置器异步,因此我们必须使其同步,我们可以做的是将 Values 属性更改为某种类型,以隐藏它的数据来自异步方法的事实实现细节并以同步方式构造此类型。

来自 Stephen Cleary 的 Mvvm.Async 库的NotifyTask 将帮助我们解决这个问题,我们要做的是将 Values 属性更改为:

private NotifyTask<List<string>> _notifyValuesTask;

public NotifyTask<List<string>> NotifyValuesTask
{
    get
    {  
        return _notifyValuesTask;
    }
    private set 
    {
        _notifyValuesTask = value;
        OnPropertyChange();
    }
}

并更改它的绑定:

<!-- Busy indicator -->
<Label Content="Loading values" Visibility="{Binding notifyValuesTask.IsNotCompleted,
  Converter={StaticResource BooleanToVisibilityConverter}}"/>
<!-- Values -->
<ListBox ItemsSource="{Binding NotifyValuesTask.Result}" Visibility="{Binding
  NotifyValuesTask.IsSuccessfullyCompleted,
  Converter={StaticResource BooleanToVisibilityConverter}}"/>
<!-- Exception details -->
<Label Content="{Binding NotifyValuesTask.ErrorMessage}"
  Visibility="{Binding NotifyValuesTask.IsFaulted,
  Converter={StaticResource BooleanToVisibilityConverter}}"/>

通过这种方式,我们创建了一个表示 Task 类似类型的属性,该类型是为数据绑定定制的,包括繁忙指示器和错误传播,有关 this MSDN articale 中的 NotifyTask 用法的更多信息(注意 NotifyTask 正在考虑那里是NotifyTaskCompletion)。

现在最后一部分是更改Filter属性设置器,以在每次更改过滤器时将notifyValuesTask设置为新的NotifyTask,并进行相关的异步操作(无需await任何东西,所有监控都已嵌入NotifyTask):

private string _filter;

public string Filter
{
    get 
    { 
        return _filter; 
    }
    set
    {
        _filter = value;
        // Construct new NotifyTask object that will monitor the async task completion
        NotifyValuesTask = NotifyTask.Create(GetValuesFromWebApi(_filter));
        OnPropertyChange();
    }
}

您还应该注意到 GetValuesFromWebApi 方法会阻塞,它会使您的 UI 冻结,您不应在调用 GetAsync 后使用 Result 属性,而是使用 await 两次:

public async Task<string> GetValuesFromWebApi(string query)
{
    var url = $"http://localhost:57157/api/v1/test/result/{query}";
    using(var response = await _httpClient.GetAsync(url))
    {
        return await response.Content.ReadAsStringAsync();
    }
}

【讨论】:

  • 感谢您的回答和清晰的示例。它看起来像我需要的,但NotifyTask&lt;T&gt; 没有公共 ctor。有什么解决方法吗?
  • @Nicolas 使用Create static method,我编辑了答案中的代码以使用它。
  • 感谢您的评论。它可以工作,但不能在异步模式下工作,当我更改过滤器时,它会冻结我的 UI,直到它从 Web API 获得结果。并且NotifyValuesTask.IsNotCompleted上的绑定不起作用,在等待web api请求结果时不显示标签,可能是第一点不起作用。
  • @Nicolas 听起来您用于从 Web API 检索结果的方法是阻塞的,并不是真正异步的,但我无法详细说明,因为您没有发布它的实现。
  • 感谢帮助我。我添加了被调用的异步方法的实现。
【解决方案2】:

你可以这样做。请注意,在“async void”中,您需要处理所有异常。否则,应用程序可能会崩溃。

public class MyClass: INotifyPropertyChanged
{
    private string _filter;
    public string Filter
    {
        get { return _filter; }
        set
        {
             RaisePropertyChanged("Filter");
            _filter = value;
        }
    }
    public MyClass()
    {
        this.PropertyChanged += MyClass_PropertyChanged;
    }

    private async void MyClass_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(Filter))
        {
            try
            {
                // call to an async WEB API to get values from the filter
                var values = await GetValuesFromWebApi(Filter);
                DisplayValues(values);
            }
            catch(Exception ex)
            {

            }
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
    void RaisePropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }

【讨论】:

  • "使用 async void 方法,异步操作引发的任何错误都会默认使应用程序崩溃。...处理错误的方法都不合适。虽然可以通过捕获来处理这个问题异步操作和更新其他数据绑定属性的异常,这将导致大量繁琐的代码。”来自Async Programming : Patterns for Asynchronous MVVM Applications: Data Binding
  • 确实如此,但您的情况不同。那是从另一个调用的异步 void 方法。如果您不传播错误,则适用于 event handlers 的异步 void。
猜你喜欢
  • 1970-01-01
  • 2017-11-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-06
  • 1970-01-01
  • 2020-05-17
相关资源
最近更新 更多