Created
May 30, 2021 10:50
-
-
Save nick-beer/0ed387322b8f44183520428f19292564 to your computer and use it in GitHub Desktop.
Using PushFrame in a WPF application to prevent UI thread deadlock while synchronously waiting for a task.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Diagnostics; | |
using System.Threading; | |
using System.Threading.Tasks; | |
using System.Windows; | |
using System.Windows.Threading; | |
namespace WpfApp3 | |
{ | |
/// <summary> | |
/// Interaction logic for MainWindow.xaml | |
/// </summary> | |
public partial class MainWindow : Window | |
{ | |
private void SomeSynchronousEntryPoint() | |
{ | |
bool ranOnUI; | |
var deadlock = false; | |
if (deadlock) | |
{ | |
// The following will always deadlock - not really any way around it... | |
ranOnUI = DoBackgroundWorkThenUIWorkAsync(Dispatcher, CancellationToken.None).Result; | |
} | |
else | |
{ | |
var task = DoBackgroundWorkThenUIWorkAsync(Dispatcher, CancellationToken.None); | |
ranOnUI = PushFrameUntilComplete(Dispatcher, task, CancellationToken.None); | |
} | |
Debug.Assert(ranOnUI); | |
} | |
private async Task<bool> DoBackgroundWorkThenUIWorkAsync(Dispatcher dispatcher, CancellationToken token) | |
{ | |
return await Task.Run(async () => | |
{ | |
var ranOnUI = false; | |
await Task.Yield(); | |
await dispatcher.BeginInvoke(() => | |
{ | |
token.ThrowIfCancellationRequested(); | |
ranOnUI = dispatcher.CheckAccess(); | |
}); | |
return ranOnUI; | |
}); | |
} | |
private static T PushFrameUntilComplete<T>(Dispatcher dispatcher, Task<T> task, CancellationToken token) | |
{ | |
// For us, the exitWhenRequested parameter here is important. The default is 'true', which means | |
// that if the shutdown has been requested for the dispatcher, this frame will exit. By passing | |
// this paramter, we get explicit control over the 'frame.Continue' property, allowing us to pump | |
// messages (and thus process UI Activities) after shutdown has been requested for our dispatcher. | |
var frame = new DispatcherFrame(exitWhenRequested: false); | |
using (token.Register(() => frame.Continue = false)) | |
{ | |
var op = dispatcher.InvokeAsync( | |
async () => | |
{ | |
try | |
{ | |
return await task; | |
} | |
finally | |
{ | |
frame.Continue = false; | |
} | |
}); | |
Dispatcher.PushFrame(frame); | |
var asyncTask = op.Task.Unwrap(); | |
return asyncTask.IsCompletedSuccessfully | |
? asyncTask.GetAwaiter().GetResult() | |
: throw new OperationCanceledException(); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Not intended to be a complete/runnable example - only to demonstrate a concept.