r9s
r9s

Reputation: 237

How to call an async method in viewmodel

I'm trying to learn MVVM pattern in WPF application. I wrote this async method in my viewmodel (it has to be async since I'm using HttpClient and it's methods are async):

public async Task<Dictionary<int, BusStop>> GetBusStops()
    {
        var busStopDict = new Dictionary<int, BusStop>();
        var url = "my url";

        using (HttpClient client = new HttpClient())
        using (HttpResponseMessage response = await client.GetAsync(url))
        using (HttpContent content = response.Content)
        {
            string data = await content.ReadAsStringAsync();
            var regularExpression = Regex.Match(data, "\\[(.)*\\]");
            var result = regularExpression.Groups[0];

            var json = JValue.Parse(result.ToString());
            var jsonArray = json.ToArray();

            foreach (var a in jsonArray)
            {
                // irrelevant logic

                busStopDict.Add(nr, bs);
            }
        }

        return busStopDict;
    }

This methods returns a dictionary filled with bus stops (my model). I would like to bind this dictionary with combobox in view, but I can't get it work, because I can't call this async method in constructor of my viewmodel and I have no idea where can I call it. Do you have any suggestions?

Upvotes: 3

Views: 13345

Answers (5)

Andrew D. Bond
Andrew D. Bond

Reputation: 1300

Microsoft's MVVM Community Toolkit has an implementation of this, and references the name NotifyTaskCompletion, the same name from Stephen Cleary's 2014 Microsoft article on asynchronous data binding.

(Stephen Cleary also maintains his own implementation of this, linked to from his answer to this same question.)

Upvotes: 0

Stephen Cleary
Stephen Cleary

Reputation: 457217

You should use asynchronous data binding (I have an entire article on the subject).

Using NotifyTask from my Mvvm.Async library, it could look like this:

public async Task<Dictionary<int, BusStop>> GetBusStopsAsync() { ... }
public NotifyTask<Dictionary<int, BusStop>> BusStops { get; }

MyViewModelConstructor()
{
  BusStops = NotifyTask.Create(() => GetBusStopsAsync());
}

Then your view can model-bind to BusStops.Result to get the dictionary (or null if it isn't retrieved yet), and also data-bind to BusStops.IsNotCompleted/BusStops.IsFaulted for busy spinners / error indicators.

Upvotes: 3

Kevin B Burns
Kevin B Burns

Reputation: 1067

I would check out AsycLazy or check out AsyncCommands and create a async Task based "LoadCommand". You shouldn't put much logic into a contructor as it will make it tougher to debug, forces you to strongly couple and will make it very difficult to write Unit tests for your view Model. I tend to make everything lazy if I can.

AsyncLazy
http://blog.stephencleary.com/2012/08/asynchronous-lazy-initialization.html

AsyncCommand
http://mike-ward.net/2013/08/09/asynccommand-implementation-in-wpf/

Upvotes: 1

NtFreX
NtFreX

Reputation: 11367

I would not recommend to write logic in your viewmodel constructor. Instead I would create an Loaded event trigger in your view also to ensure you do not interfere with the loading procedure of the view.

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"


<i:Interaction.Triggers>
    <i:EventTrigger EventName="Loaded">
        <i:InvokeCommandAction Command="{Binding LoadedCommand}" />
    </i:EventTrigger>
</i:Interaction.Triggers>

Then in your viewmodel I recomend doing the following:

Add the following property for your Loaded event

public DelegateCommand LoadedCommand { get; }

Then assign it in your constructor

LoadedCommand = new DelegateCommand(async () => await ExecuteLoadedCommandAsync());

Add the loaded method and call your method within it

private async Task ExecuteLoadedCommandAsync()
{
    var busStops = await GetBusStops();
    //TODO: display the busStops or do something else
}

Furthermore is adding "Async" as suffix to your asynchron methods names a good naming pattern. It enables you to quickly see which methods are asynchron. (so rename "GetBusStops" to "GetBusStopsAsync")

This is a simple DelegateCommand implementation

public class DelegateCommand : ICommand
{
    private readonly Predicate<object> _canExecute;
    private readonly Action<object> _execute;

    public event EventHandler CanExecuteChanged;

    public DelegateCommand(Action<object> execute) 
               : this(execute, null)
    {
    }

    public DelegateCommand(Action<object> execute, 
               Predicate<object> canExecute)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public override bool CanExecute(object parameter)
    {
        if (_canExecute == null)
        {
            return true;
        }

        return _canExecute(parameter);
    }

    public override void Execute(object parameter)
    {
        _execute(parameter);
    }

    public void RaiseCanExecuteChanged()
    {
        if( CanExecuteChanged != null )
        {
            CanExecuteChanged(this, EventArgs.Empty);
        }
    }
}

When using this implementation you need to change your initialising of the DelegateCommand in your viewmodel constructor to the following

LoadedCommand = new DelegateCommand(async (param) => await ExecuteLoadedCommandAsync());

Upvotes: 9

Fruchtzwerg
Fruchtzwerg

Reputation: 11399

Start your async method in the constructor and define an action to be continued with like.

//Constructor
public ViewModel()
{
    GetBusStops().ContinueWith((BusStops) =>
    {
        //This anonym method is called async after you got the BusStops
        //Do what ever you need with the BusStops
    });
}

Don´t forget to invoke the UI thread if you want to access an property used for the View with

Application.Current.Dispatcher.BeginInvoke(() =>
{
    //Your code here
});

Upvotes: 1

Related Questions