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:
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.