1

Please help me understand how to properly await long executing tasks to keep the UI responsive in a Universal Windows application.

In the code below OperateSystem is a model class which inherits ObservableObject. OperateSystem.GetLatestDataFromAllDevices connects to a variety of instruments, collects data, and updates class properties with the information from the instruments. The views update with values from Operate System.

The UI is not responsive while the dispatcher.RunAsync task is running, I added a Thread.Sleep(5000) to GetLatestDataFromAllDevices() to make sure and it locks up the UI for 5 seconds. Without the await Task.Delay(refreshTimer) the UI never updates (I'm assuming it instantly goes back into the GetLatestDataFromAllDevies before the UI can update). Setting the refreshTimer to 1ms allows the UI to update, but I know that's a workaround for another issue that needs to be fixed.

   public ProductionViewModel()
    {
        OperateSystem = new OperateSystem();
        StartButtonCommand = new RelayCommand(StartMeasurementSystem);
        StopButtonCommand = new RelayCommand(StopMeasurementSystem);

        if (!Windows.ApplicationModel.DesignMode.DesignModeEnabled)
        {
            dispatcher = CoreWindow.GetForCurrentThread().Dispatcher;
        }
    }

    private async void StartMeasurementSystem()
    {
            stopRequest = false;
            StopButtonEnabled = true;
            StartButtonEnabled = false;
            while (!stopRequest)
            {
                await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => OperateSystem.GetLatestDataFromAllDevices(ConfigurationSettingsInstance));
                await Task.Delay(refreshTimer);
            }
    }

In OperateSystem

    internal void GetLatestDataFromAllDevices(ConfigurationSettings configurationSettings)
    {
        GetDataInstrument1(configurationSettings);
        GetDataInstrument2(configurationSettings);
        GetDataInstrument3(configurationSettings);
        GetDatainstrumetn4(configurationSettings);
    }

Each of the GetDataInstrumnet methods connect to an instrument, gathers data, performs some scaling/formatting, and updates a class property with the current value.

I followed other S.O. answers to use the dispatcher.RunAsync as using other async methods I would get thread mashalling errors. But now I think the dispatcher is just marshaling these tasks on the UI thread anyway so it still blocks UI udpates.

To recreate the thread marshalling errors, I made GetLatestDataFromAllDevices async, and awaited a method executed as a task.

    internal async void GetLatestDataFromAllDevices(ConfigurationSettings configurationSettings)
    {
        await Task.Run(()=>GetDataInstrument1(configurationSettings));
        GetDataInstrument2(configurationSettings);
        GetDataInstrument3(configurationSettings);
        GetDatainstrumetn4(configurationSettings);
    }

This results in: System.Exception: 'The application called an interface that was marshalled for a different thread. (Exception from HRESULT: 0x8001010E (RPC_E_WRONG_THREAD))'

I've refactored in circles a few times and keep running into either thread marshaling errors or an unresponsive UI, what's a good way to get this done?

Singletrax
  • 13
  • 2
  • `I added a Thread.Sleep(5000)` - that would be correct, because the work is [scheduled on the UI thread](https://learn.microsoft.com/en-us/uwp/api/windows.ui.core.coredispatcher.runasync?view=winrt-19041), which you block with the Sleep. It's better to do [the other way round](https://stackoverflow.com/a/19138262/11683). You cannot meaningfully await a `void` either. – GSerg May 28 '20 at 15:35
  • 1
    Stephen Cleary's answer on that thread is what I'm after (your first link). The accepted answer is having background tasks wait for a UI task to complete, Stephen points out "You'll usually find your code is much cleaner and easier to understand (and it will definitely be more portable) if you have the UI be the "master" and the background threads be the "slaves"." Right on! But when I use Task.Run() as he suggests, which runs these tasks on another thread (Not the UI thread) I get the thread marshaling errors... It's not clicking yet how to get this done... – Singletrax May 28 '20 at 16:07
  • Is the problem i'm seeing with thread marshaling using Task.Run() that I am updating properties bound to the Views in the model which i'm trying to run on another thread? I could make properties on the view model for every model property that will be displayed, then fire a method to update the view model properties after the model tasks complete. That feels like a lot of redundant properties but i'm all for it if that's the cleaner approach. Or is it still not clicking... – Singletrax May 28 '20 at 16:13
  • 1
    You run non UI code in `Task.Run` and return data for the UI thread to use. Going back and forth will be harder to code, harder to debug and slower. – Paulo Morgado May 28 '20 at 16:37
  • Paulo and GSerg thanks for the pointers! Also noted on Steve's answer below, I removed any of the UI property updates from any of the methods I was trying to Task.Run(()=>...) and it works great. I think I'm understanding that I was trying to update a property that exists on the UI thread from another thread. I can do those updates outside of the async tasks since they won't block. – Singletrax May 28 '20 at 19:30

1 Answers1

3

I've refactored in circles a few times and keep running into either thread marshaling errors or an unresponsive UI, what's a good way to get this done?

Since you have an unresponsive UI, you must push the work to a background thread (e.g., Task.Run).

For marshalling updates back to the UI thread, I recommend (in order of preference):

  1. Using the return value of asynchronous methods. E.g., MyUiProperty = await Task.Run(() => MyBackgroundMethod(...));.
  2. Using Progress<T> to get multiple values from asynchronous methods. E.g., var progress = new Progress<string>(update => MyUiProperty = update); await Task.Run(() => MyBackgroundMethod(..., progress));.
  3. Capturing a SynchronizationContext in your background classes and using that for sending updates to the UI thread. This is the least recommended because it results in your background driving your UI instead of the other way around.
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1
    Thanks Stephen! I am gathering my issue with using await Task.Run(()=>.. was that I was also updating class properties which the UI binds to in those tasks. If I'm understanding what was happening it was updating properties on a background thread but the view binding / observable object workings are all handled on the UI thread. I pulled any updates to properties from the GetDataInstrument*() methods and it works without binding! – Singletrax May 28 '20 at 19:27