Richiban
Richiban

Reputation: 5930

How to get a Task to return to the UI thread in WPF

Background: I'm relatively experienced in C#, but completely new to WPF.

I have a WPF application that will be used internally for some simple monitoring. I have a database call and the data that's returned is then displayed in a tree view, and while this happens there's an overlay that reads "Loading..." before the data comes back. My current implementation looks like this:

await WithOverlay("Loading...", async () =>
{
    MyControl.Items = await _database.Retrieve(messageSummary.Id);
});

where WithOverlay looks like this:

private async Task WithOverlay(string overlayMessage, Func<Task> func)
{
    Overlay.Content = overlayMessage;
    Overlay.Visibility = Visibility.Visible;

    await func();

    Overlay.Visibility = Visibility.Hidden;
}

Now, this works just fine, but since a lot of times (depending on what exactly the user is looking at elsewhere in the application) the database call comes back really quickly the "Loading..." overlay just appears as an annoying flicker. It's only a small thing but it's bothering me so I decided that I could fix it by putting in a small delay before the overlay appears; say, a quarter of a second. This was my attempt at modifying the WithOverlay method:

private async Task WithOverlay(string overlayMessage, Func<Task> func)
{
    var tokenSource = new CancellationTokenSource();
    var token = tokenSource.Token;

    var overlayTask = Task.Delay(250, token).ContinueWith(_ => {
        Overlay.Content = overlayMessage;
        Overlay.Visibility = Visibility.Visible;
    });

    await func();

    if (token.CanBeCanceled) tokenSource.Cancel();

    Overlay.Visibility = Visibility.Hidden;
}

My idea was to:

Unfortunately though this doesn't work; on the line Overlay.Content = overlayMessage I get the exception "The calling thread cannot access this object because a different thread owns it.'".

I have a suspicion that this has something to do with the Task's synchronisation context (if I recall my tech demos correctly) but I can't figure out how to control that to get the continuation to resume on the same thread.

Upvotes: 1

Views: 1496

Answers (2)

Richiban
Richiban

Reputation: 5930

Thanks for your answer, @mm8. Okay, so I accomplished it like this, I'd be interested to hear your thoughts on whether this is worse than your anwser:

    private async Task WithOverlay(string overlayMessage, Func<Task> func)
    {
        var delayTask = Task.Delay(250);
        var wrappedTask = func();

        var completedTask = await Task.WhenAny(delayTask, wrappedTask);

        if (completedTask == delayTask)
        {
            Overlay.Content = overlayMessage;
            Overlay.Visibility = Visibility.Visible;
        }

        await wrappedTask;

        Overlay.Visibility = Visibility.Hidden;
    }

So, I start a task that is just a 250ms delay. I start the wrapped task, then see which one finished first using Task.WhenAny; if the delay task finished first then I display the overlay. Then I await the wrappedTask and hide the overlay.

Upvotes: 2

mm8
mm8

Reputation: 169200

You can associate a TaskScheduler with the continuation task to force the delegate that sets the Content and Visibility properties to be set on the UI thread:

var overlayTask = Task.Delay(250, token).ContinueWith(_ => {
    Overlay.Content = overlayMessage;
    Overlay.Visibility = Visibility.Visible;
}, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());

Upvotes: 2

Related Questions