Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

WinUI 3 – Updating UI Thead From Inside Async Method

Background

I am developing an app using WinUI 3. At one spot, the user clicks a button to load data from an API, and the logic pulls the data in chunks. I want to show the status, with a bound TextBlock that shows the current number of records pulled in from the API as it’s running, until it finishes and retrieves everything.

Note: This is WinUI 3. It is not WPF, and it is not UWP. There are lots of answers for how to accomplish what I need to accomplish in WPF and UWP. But WinUI 3 seems to be just different enough to not work.

My Code

First Attempt

LoadData = new RelayCommand<object>(
    async (parameter) =>
    {

        this.CurrentJob.Status = "Initializing";

        await Task.Run(() =>
        {
            this._jobService.LoadData(
                jobToRun: this.CurrentJob,
                onProgressCallback: ((countRecordsLoaded) => {
                    this.CurrentJob.Status = "Running";
                    this.CurrentJob.ExportCount = countRecordsLoaded;
                })
            );;
        });

        this.CurrentJob.Status = "Complete";

    }

);

This fails with an error:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

System.Runtime.InteropServices.COMException: ‘The application called an interface that was marshalled for a different thread. (0x8001010E (RPC_E_WRONG_THREAD))’

So I researched, and found the DispatcherQueue.

Second Attempt

LoadData = new RelayCommand<object>(
    async (parameter) =>
    {

        this.CurrentJob.Status = "Initializing";

        dispatcherQueue.TryEnqueue(() =>
        {
            this._jobService.LoadData(
                jobToRun: this.CurrentJob,
                onProgressCallback: ((countRecordsLoaded) => {
                    this.CurrentJob.Status = "Running";
                    this.CurrentJob.ExportCount = countRecordsLoaded;
                })
            );;
        });

        this.CurrentJob.Status = "Complete";

    }

);

This just runs synchronously, which won’t work for me because it can sometimes take 30seconds or more.

Third Attempt (Nesting Task.Run inside DispatcherQueue)

LoadData = new RelayCommand<object>(
    async (parameter) =>
    {

        this.CurrentJob.Status = "Initializing";

        dispatcherQueue.TryEnqueue(() =>
        {
            _ = Task.Run(() =>
            {
                this._jobService.LoadData(
                    jobToRun: this.CurrentJob,
                    onProgressCallback: ((countRecordsLoaded) => {
                        this.CurrentJob.Status = "Running";
                        this.CurrentJob.ExportCount = countRecordsLoaded;
                    })
                );;
            });
        });

        this.CurrentJob.Status = "Complete";

    }

);

This goes back to the first error.

More Attempts

I tried using both variations (DispatcherQueue.TryEnqueue and DispatcherQueue.EnqueueAsync). They seem to function identically. I’ve tried awaiting and not awaiting both inside and outside. I’ve tried putting the dispatcherqueue part inside the task.run. I can get it to error out, and I can get it to run synchronously. But I can’t get it to update the UI from within something running async.

Question

How do update the UI thread from inside a truly async method?

>Solution :

The standard solution for this (on all .NET UI platforms) is progress reporting. Ideally, LoadData would take an IProgres<T> instead of a callback method, but you can work with a lambda, too.

LoadData = new RelayCommand<object>(
    async (parameter) =>
    {
        this.CurrentJob.Status = "Initializing";
        IProgress<int> progress = new Progress<int>(countRecordsLoaded =>
        {
            this.CurrentJob.Status = "Running";
            this.CurrentJob.ExportCount = countRecordsLoaded;
        });

        await Task.Run(() =>
        {
            this._jobService.LoadData(
                jobToRun: this.CurrentJob,
                onProgressCallback: progress.Report
            );
        });

        this.CurrentJob.Status = "Complete";
    }
);

What’s actually happening is that Progress<T> captures the current synchronization context (which is tied to the dispatcher queue on WinUI). Then, whenever a background thread calls Report, it queues up that update to the UI thread instead of running it directly.

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading