Paulus Limma
Paulus Limma

Reputation: 452

MVVM How to set datacontext when viewmodel uses async

After hours of searching I am still without answer to this question. I have read this nice writing about async MVVM and made my viewmodel to use factory method.

public class MainViewModel
{
    // sic - public, contrary to the pattern in the article I cite
    // so I can create it in the Xaml as below
    public MainViewModel() 
    {
    }

    private async Task InitializeAsync()
    {
        await DoSomethingAsync();
    }

    public static async Task<MainViewModel> CreateAsync()
    {
        var ret = new MainViewModel();
        await ret.InitializeAsync();
        return ret;
    }
}

This is clear for me, but I can't understand how to make instance of MainViewModel and set it to datacontext in MainPage. I can't simply write

<Page.DataContext>
    <viewModel:MainViewModel/>
</Page.DataContext>

because I should use MainViewModel.CreateAsync()-method. And I can't do it on code-behind, which I even want to do, because code-behind -constructor is normal method, not an async-method. So which is proper way to continue?

Upvotes: 5

Views: 3448

Answers (4)

Snicker
Snicker

Reputation: 987

You have to initalize the viewmodel before the window is open. Go to your App.xaml file and remove the part: StartupUri="MainWindow.xaml". Then you go to the App.xaml.cs and add this:

protected async override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);
    var mainWindow = new MainWindow { DataContext = await CreateAsync() };
    mainWindow.Show();
}

Upvotes: 3

Hamlet Hakobyan
Hamlet Hakobyan

Reputation: 33381

Firstly, you should make default constructor as private to avoid misusing your class (the article you cite does this - the constructor is private).

The approach you are using to set DataContext is not suitable for MVVM pattern (the View shouldn't create its ViewModel itself). You should create your View and ViewModel in the higher level layer and have that layer bind them. Says if the Page is your main View you should create them in App.xaml.cs by overriding OnStartup, something like this:

var page = new Page();
var dataService = new YourDataService(); // iff Create or the ctor require arguments
var viewModel = await MainViewModel.CreateAsync(dataService);
page.DataContext = viewModel;
page.Show();

Upvotes: 2

Stephen Cleary
Stephen Cleary

Reputation: 456437

made my viewmodel to use factory method

I'm normally a fan of that approach - it's my favorite way to work around the "no async constructors" limitation. However, it doesn't work well in the MVVM pattern.

This is because VMs are your UI, logically speaking. And when a user navigates to a screen in an app, the app needs to respond immediately (synchronously). It doesn't necessarily have to display anything useful, but it does need to display something. For this reason, VM construction must be synchronous.

So, instead of trying to asynchronously construct your VM, first decide what you want your "loading" or "incomplete" UI to look like. Your (synchronous) VM constructor should initialize to that state, and it can kick off some asynchronous work that updates the VM when it completes.

This is not too hard to do by hand, or you can use the NotifyTaskCompletion approach that I described in an MSDN article on async MVVM data binding to drive the state transition using data bindings.

Upvotes: 8

h.alex
h.alex

Reputation: 902

I would re-factor. Make the MainViewModel construction / instantiation lightweight. Then create a Load or Initialize method on your VM. From the code-behind create an instance, set it to the DataContext, then invoke the init method and let it run.

E.g.

/// <summary>Interaction logic for MainWindow.xaml</summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        this.InitializeComponent();

        var dc = new MainViewModel();
        dc.Initialize("Hello", " ", "world");
        this.DataContext = dc;
    }
}

public class MainViewModel
{
    /// <summary>Simple constructor</summary>
    public MainViewModel() { }

    public void Initialize(params object[] arguments)
    {
        //use the task to properly start a new thread as per:
        //http://stackoverflow.com/a/14904107/1144090 and
        //https://msdn.microsoft.com/en-us/library/hh965065.aspx
        //(what would happen if we simply invoke init async here?)
        this.InitializeAsync(arguments)
            .ContinueWith(result =>
            {
                if (!result.IsFaulted)
                    return;

                MessageBox.Show("Unexpected error: " + Environment.NewLine + result.Exception.ToString());
            });
    }

    private async Task InitializeAsync(params object[] arguments)
    {
        await Task.Delay(2333);

        MessageBox.Show(String.Concat(arguments));
    }
}

Note that this is the quick-and-dirty solution, the other two answers (paired with a dependency injection framework) will give you proper high-level structure for your solution.

Upvotes: 2

Related Questions