I have a basic question about MVVM practice ,it is not advisable to call model in setter of a viewmodel property which is binded to a UI control.
public int Woo
{
get
{
return _Woo;
}
set
{
_Woo=model.SetWoo(value);
NotifyPropertyChange("Woo");
}
}
Instead people advice to set the property in model and send an event to viewmodel to refresh its property with new value and eventually refresh the UI control also.Because the abovementioned code will block the UI control if model's method takes lot of time.
My question is even if I send the event from model ,UI thread is still blocked till execution of its subscribed delegate finishes.So What is the difference between these two approaches and which one is correct?
I am always using the approach above in order to not to block UI thread.
public int Woo
{
get
{
_Woo;
}
set
{
BackgroundWorker backgroundWorker = new BackgroundWorker();
//DoWork is ran on separate thread and does not block UI
backgroundWorker.DoWork += (sender, arguments) =>
{
arguments.Result = model.GetWoo(); //Store data in worker result
}
//This method runs definitely on UI
backgroundWorker.RunWorkerCompleted += (sender, arguments) =>
{
//Get stored result and assign to UI-bound stuff
_Woo = arguments.Result;
NotifyPropertyChange("Woo");
}
backgroundWorker.RunWorkerAsync();
}
}
MVVM is concerned with the separation of concerns, it does not describe how threading works with respect to events and event-handling. Executing code in the UI thread is inevitable, the trick is to only perform fast operations in the UI thread, if something is going to take longer than 50ms then it is your responsibility, as the programmer/developer, to spin-off a new thread (or thread abstraction, such as a Task
) to perform the task and notify the UI thread when the operation is complete.
Ideally what you want to do, is do as much heavy lifting as possible on a different thread to the UI thread. You then use some strategy to join back onto the UI thread to apply the result of the heavy lifting.
The simplest approach I've found is to use a Task with a continuation, using 2 different TaskSchedulers. I think there is an easier way using async/await, but I'm not familiar enough with it to comment with any authority.
Using your example:
private int? _Woo;
public int? Woo
{
get
{
if (!_Woo.HasValue)
{
var task = Task.Factory.StartNew<int>(model.GetWoo, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default());
task.ContinueWith(a => { _Woo = a.Result; }, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.FromCurrentSynchronizationContext());
}
return _Woo;
}
set
{
_Woo = value;
NotifyPropertyChange("Woo");
}
}
Note that the heavy lifting task is scheduled using the Default TaskScheduler (meaning it will execute on a thread obtained from the ThreadPool) while the continuation is scheduled using the current synchronization context TaskScheduler (meaning it will execute back on whatever thread originally called Get).
Usually I would inject the 2 TaskSchedulers (Background and Foreground/UI) into the ViewModel so that I could control the scheduling strategy for unit tests, but I've just hard referenced Default and FromCurrentSynchronizationContext in the example above.
(Side Note: I'm not entirely sure what your sample code was trying to accomplish, if anything. I switched it up so that it is now essentially an asynchronous Get. The UI will ask for the value of Woo, which will trigger an asynchronous refresh using a Task and return instantly with its current NULL value. As soon as GetWoo finishes, it will set the value of Woo on the View Model, which will trigger a Property Changed event (on the correct thread), which will cause the UI to update the value of Woo on the screen. I think it shows the concept of doing the heaving lifting off the UI thread anyway.)
You can use BackgroundWorker if you want to update UI. like below is example.
var worker = new BackgroundWorker();
worker.DoWork += (o, ea) =>
{
Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new Action(
() =>
{
//you code for update UI
}));
};
worker.RunWorkerCompleted += (o, ea) =>
{
};
worker.RunWorkerAsync();