5

Here is my problem:
I have a WPF TextBox binded on the property Filter. It works as a filter: each time the TextBox.Text changes, the Filter property is set.

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

Now on the ViewModel there is my Filter property: each time the filter changes I update my values.

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}");
    }
}

As it is not allowed to use async property, what could I do to make my binding working if it needs to call an async method?

Nicolas
  • 6,289
  • 4
  • 36
  • 51
  • Possible duplicate of [Async property in c#](https://stackoverflow.com/questions/12384309/async-property-in-c-sharp) – JSteward Jan 11 '18 at 19:16
  • Rearrange your code so that you don't need to call async method from your property setter... – Dean Kuga Jan 11 '18 at 19:39
  • If you do a lot of things in a function, especially things that might go wrong, reconsider your design to make it a property. Make it a function instead. – Harald Coppoolse Jan 12 '18 at 09:26

3 Answers3

7

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();
    }
}

And it's bindings:

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

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();
    }
}

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}}"/>

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();
    }
}

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();
    }
}
YuvShap
  • 3,825
  • 2
  • 10
  • 24
  • thanks for your answer and your clear examples. It looks like what I need, but `NotifyTask` doesn't have a public ctor. Any workaround? – Nicolas Jan 16 '18 at 17:07
  • @Nicolas use [Create static method](http://dotnetapis.com/pkg/Nito.Mvvm.Async/1.0.0-pre-03/netstandard2.0/doc/Nito.Mvvm.NotifyTask/Create''1(System.Threading.Tasks.Task(''0),''0)), I edited the code in the answer to use it. – YuvShap Jan 16 '18 at 18:22
  • thanks for your comment. It works but not in async mode, when I change my filter it freezes my UI until it gets the result from the web API. And the binding on NotifyValuesTask.IsNotCompleted doesn't work, it doesn't display the label while waiting for the result of the web api request, probably because the first point doesn't work. – Nicolas Jan 18 '18 at 10:58
  • @Nicolas it sounds like the method you are using to retrieve the results from the web API is blocking and is not really asynchronous, but I can't elaborate more since you didn't post it's implementation. – YuvShap Jan 18 '18 at 11:25
  • thanks helping me. I added the implementation of the called async method. – Nicolas Jan 18 '18 at 13:06
  • thanks to your last comment, I changed the content of the called async method. Now it works as expected. Thanks a lot buddy! – Nicolas Jan 18 '18 at 13:13
3

You can do it like this. Beware that in "async void" you need to handle all exceptions. If you don't, the application can crash.

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));
    }
sskodje
  • 129
  • 1
  • 11
  • "With an async void method, any errors raised by the asynchronous operation will crash the application by default. ...Neither method of handling errors is appropriate. And though it’s possible to deal with this by catching exceptions from the asynchronous operation and updating other data-bound properties, that would result in a lot of tedious code." From [Async Programming : Patterns for Asynchronous MVVM Applications: Data Binding](https://msdn.microsoft.com/en-us/magazine/dn605875.aspx) – YuvShap Jan 12 '18 at 06:29
  • Indeed, but your scenario is different. That is an async void method that is called from another. Async void is appropriate from [event handlers](https://channel9.msdn.com/Series/Three-Essential-Tips-for-Async/Tip-1-Async-void-is-for-top-level-event-handlers-only), if you don't propagate errors. – sskodje Jan 13 '18 at 12:50
0

This works from a property setter & I use it:

set
{
                 
  System.Windows.Application.Current.Dispatcher.Invoke(async () =>{ await this.SomeMethodAsync(); });
}

with the async method for clarity:

Task SomeMethodAsync()
{
  //fill in whatever fits your scenario
   await Task.Run(()=> blah blah blah);
}
JRrelyea
  • 139
  • 1
  • 4