Reputation: 5930
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
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
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