C#, MVVM, Tasks and the UI Thread

13,793

Solution 1

There could be any number of reasons why the continuation is running on the UI thread. The MVVM framework could be helping, or something else is making it run on the UI thread, or you're just getting lucky.

To ensure the continuation runs on the UI thread you can capture the UI TaskScheduler right before like so.

var uiScheduler = TaskScheduler.FromCurrentSyncronizationContext();

Task.Factory.StartNew(() =>
    UiDataProvider.RefreshForwardContractReport(fcrIdField.Value),
    TaskCreationOptions.LongRunning
) // Ensures the task runs in a new thread
.ContinueWith(task => {
    if (task.Result.OperationSuccess)
    {
        RefreshReport(task.Result.OperationResult);
    }
}, uiScheduler); // Runs the continuation on the UI thread.

This assumes that the outer method is run from the UI to begin with. Otherwise you could capture the UI scheduler at a top level and access it globally in your app.

If you can use async/await then the code becomes much easier.

var result = await Task.Factory.StartNew(
    () => UiDataProvider.RefreshForwardContractReport(fcrIdField.Value),
    TaskCreationOptions.LongRunning
);

if (result.OperationSuccess)
{
    RefreshReport(result.OperationResult);
}

Solution 2

WPF keeps allowing more cross-thread operations with each release. Other MVVM platforms do not allow them at all. In the case of ObservableCollection, unless you are using EnableCollectionSynchronization, updating from a background thread is incorrect.

I agree with @avo in that you should treat your ViewModel (your logical UI) as though it had UI thread affinity (like the literal UI). All data binding updates should be done in the UI context.

As @Cameron pointed out, this is most easily done via async:

var result = await Task.Run(
    () => UiDataProvider.RefreshForwardContractReport(fcrIdField.Value));
if (result.OperationSuccess)
{
  RefreshReport(result.OperationResult);
}

Solution 3

It happens because WPF automatically dispatches the PropertyChanged event to the main thread, unlike all other XAML frameworks. In all other frameworks, a dispatching solution is needed. The best article I've read about MVVM and multi threading https://msdn.microsoft.com/en-us/magazine/dn630646.aspx

Solution 4

Considering the MVVM scenario, when you are in the ContinueWith part, i agree that you are in the Non-UI thread and you are updating the properties in the ViewModel (which are bound to UI elemnts) and not the UI elements itself.

Try updating the UI elements in the ContinueWith part, say like

.ContinueWith(task => myTextblock.Text = "SomeText")

In that scenario, u'll get the exception that you are expecting.

P.S - "myTextBlock" is a text block in your View.

Solution 5

You should use another (not UI) thread to retrieve long data from DB or another source for prevent UI frozen. But when you try to change UI element from not UI thread it may throw some exception. To change UI from not UI thread you should add update task to UI thread:

Deployment.Current.Dispatcher.BeginInvoke(() => 
{
   some udate action here
});  
Share:
13,793
Chris
Author by

Chris

Updated on June 04, 2022

Comments

  • Chris
    Chris almost 2 years

    We have an application built according to the MVVM pattern. At various times we kick off tasks to go to a database to retrieve data, we then populate an ObservableCollection to which a WPF control is bound with that data.

    We are a little confused, when we populate the ObservableCollection we are doing so on the task thread, not the UI thread, yet the UI is still updated/behaving correctly. We were expecting an error and to have to change the code to populate the collection on the UI thread.

    Is this a dangerous scenario and should we populate on the UI thread anyway?

    Code to get data:

    Task.Factory.StartNew(() =>
        UiDataProvider.RefreshForwardContractReport(fcrIdField.Value)
    )
    .ContinueWith(task => {
        if (task.Result.OperationSuccess)
        {
            // This updates the ObseravableCollection, should it be run on UI thread??
            RefreshReport(task.Result.OperationResult);
        }
    });