如何从属性设置器调用异步方法

Nic*_*las 3 wpf binding async-await

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

<TextBox Text="{Binding Filter, UpdateSourceTrigger=PropertyChanged, Mode=OneWayToSource}" />
Run Code Online (Sandbox Code Playgroud)

现在在 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}");
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Yuv*_*hap 6

I will assume that DisplayValues method implementation is changing a property that is bound to the UI and for the demonstration I will assume it's a List<string>:

private List<string> _values;

public List<string> Values
{
    get
    {  
        return _values;
    }
    private set 
    {
        _values = value;
        OnPropertyChange();
    }
}
Run Code Online (Sandbox Code Playgroud)

And it's bindings:

<ListBox ItemsSource="{Binding Values}"/>
Run Code Online (Sandbox Code Playgroud)

Now as you said it is not allowed to make property setters async so we will have to make it sync, what we can do instead is to change Values property to some type that will hide the fact it's data comming from asynchronous method as an implementation detail and construct this type in a sync way.

NotifyTask from Stephen Cleary's Mvvm.Async library will help us with that, what we will do is change Values property to:

private NotifyTask<List<string>> _notifyValuesTask;

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

And change it's binding:

<!-- 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}}"/>
Run Code Online (Sandbox Code Playgroud)

This way we created a property that represents a Task alike type that is customized for databinding, including both busy indicator and errors propagation, more info about NotifyTask usage in this MSDN articale (notice that NotifyTask is consider there as NotifyTaskCompletion).

Now the last part is to change Filter property setter to set notifyValuesTask to a new NotifyTask every time the filter is changed, with the relevant async operation (no need to await anything, all the monitoring is already embedded in 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

You should also notice that GetValuesFromWebApi method blocks and it will make your UI freeze, you shouldn't use Result property after calling GetAsync use await twice instead:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)