Reputation: 452
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
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
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
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
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