0

I'm trying to implement MVVM and in the ViewModel I'm doing some async fetching of data. For that purpose I've tried to loading data in the constructor:

MyModel Model { get; set; }

public MyViewModel()
{
  Model = new MyModel();
  Model.Foo = await LoadDataFromIsolatedStorage();

But this isnt valid as you cant append async to the contructor. So I tried a public static load function:

MyModel Model { get; set; }

public MyViewModel()
{
  Model = new MyModel();
}

async public static void Load()
{
  Model.Foo = await LoadDataFromIsolatedStorage();

But here WP8 complains that it Cannot await void. Because you would set up the ViewModel and bind it to the View in the code behind of the view. Boring. Lastly a fix is making the Load function return a ViewModel, so that you in the code behind of the view can do something like:

protected override void OnNavigatedTo(NavigationEventArgs e)
{
  MyViewModel viewModel = await MyViewModel.Load();

with the following code to load:

MyModel Model { get; set; }

public MyViewModel()
{
  Model = new MyModel();
}

async public static Task<MyViewModel> Load()
{
  MyViewModel viewModel = new MyViewModel();
  viewModel.Model.Foo = await LoadDataFromIsolatedStorage();
  return viewModel;

NOW, the problem at hand is that I have no control if the data loaded should force the application to navigate to another page. Lets say MyViewModel loads a variable from isolated storage, that should then make the app navigate to another page?

I've set up eventlistener to MyViewModel to make the app navigate, but I cant do this when I initiate it.

Does not work with events:

protected override void OnNavigatedTo(NavigationEventArgs e)
{
  MyViewModel viewModel = await MyViewModel.Load();
  viewModel.NavigationAction += viewmodel_NavigationAction;
}

void viewmodel_NavigationAction(sender, args)
{
  NavigationService.Navigate(...)
}

Would work but I "cannot await void":

async protected override void OnNavigatedTo(NavigationEventArgs e)
{
  MyViewModel viewModel = new MyViewModel();
  viewModel.NavigationAction += viewmodel_NavigationAction;
  await viewModel.Load(); // given the Load only is a async void and not Task<T>
}

void viewmodel_NavigationAction(sender, args)
{
  NavigationService.Navigate(...)
}
Jason94
  • 13,320
  • 37
  • 106
  • 184
  • Still reading through the question, but you can have "async void" methods, just change the return type to `Task`. – rliu Dec 01 '13 at 10:02
  • A simple hack is to just return a bool on the Load – Jason94 Dec 01 '13 at 10:02
  • What I'm saying is that you don't need to hack anything. I think .NET lets you await `void` methods, but prefers using `Task`s instead. The error message probably refers to a `void` return type for `LoadDataFromIsolatedStorage`. I don't really understand the question though. If the data loaded should force the application to navigate to another page... why don't you just navigate to another page after `Load()` is done? Heck, if that redirect can even happen, why would you make this async? The user probably doesn't want to be redirected while he/she is using the UI – rliu Dec 01 '13 at 10:09
  • 1
    Have a look at Stephen Cleary's article [*Async OOP 2: Constructors*](http://blog.stephencleary.com/2013/01/async-oop-2-constructors.html). – svick Dec 01 '13 at 11:46

2 Answers2

1

I assume this code block "doesn't work" because the event is set too late, after the data is loaded:

MyViewModel viewModel = await MyViewModel.Load();
viewModel.NavigationAction += viewmodel_NavigationAction;

In that case, fixing your last code block is simple enough: have Load return Task and it will work. Task is the async equivalent of void; you should never use async void unless you're writing an event handler. See my best practices article for more information.

However, even when you get this working, you'll end up with an empty view until the VM loads; this may be a poor user experience if your load could take a long time (or errors out, say, if there's no network connectivity).

You may want to consider using NotifyTaskCompletion from my AsyncEx library, which I describe on my blog. That pattern allows you to (immediately) create a VM in a "loading" state, which will transition (via INotifyPropertyChanged) to a "loaded" or a "loading error" state. That pattern provides a better UX, IMO.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
-1

What roliu wrote makes pretty much sense since the whole concept of async/await pattern is based on Tasks. When writing an asynchronous method you ALWAYS need to return a task to make sure the following await will work. Usually the compiler will make some special replacements in the background, so you don't have to care about it.

You will never get a bool as a result of an asynchronous operation - but a generic bool typed Task! Make sure you understood the pattern like described at MSDN.

Then you can create something like this. It is not a Win8 App. For simplicity I chose it to be a console application.

class Program
    {
        static void Main(string[] args)
        {
            DoWork();
        }

        static async void DoWork()
        {
            await YourVoidMethod();
        }

        static Task YourVoidMethod()
        {
            Task task = Task.Run(() =>
                {
                    // Your payload code
                }
            );

            return task; 
        }
    }

Just as a hint: When working with GUIs you also need to work with the dispatcher. When changing data of the UI thread you otherwise could generate cross thread exceptions.

Jan Rothkegel
  • 737
  • 3
  • 21
  • There are several problems with this answer: 1) `Main` cannot be `async` 2) You should use `Task.Run` instead of `new Task`/`Task.Start` 3) Using background tasks to initialize the viewmodel will not work in many cases because most VMs have UI thread affinity. – Stephen Cleary Dec 01 '13 at 13:24
  • [`async Main` was allowed on VS2010 with the Async CTP; it does not work on VS2012 or VS2013.](http://stackoverflow.com/q/9208921/263693) I downvoted this answer because it contains several poor practices (such as `Task.Start`, which has nothing to do with how `async` creates its tasks), and won't work for this question (since it creates/initializes the VM on a background thread). Note that a properly-written `async` UI app does not need to use the dispatcher. – Stephen Cleary Dec 01 '13 at 14:02
  • Seems like you were right. There is no way to make the async Main() work. I also fixed the Task.Start(). Do you have any other advice? I did not realize the advantage of Task.Factory.StartNew() in that context until today. Thanks :-) – Jan Rothkegel Dec 01 '13 at 15:02
  • @Stephen: I really liked your [blog story](http://blog.stephencleary.com/2013/08/startnew-is-dangerous.html) about Task.Run() vs. Task.Factory.StartNew() - I would have taken Task.Factory.StartNew() as a first choice, because it is recommended as a fast way of creating new threads in most of my Microsoft Press books. But as you stated it can confuse one. – Jan Rothkegel Dec 01 '13 at 15:18