5

I have a Windows Forms application and a Form with many async void event handlers. This form can be closed by the user at any moment. After closing the form I want to delete a temporary file, and my problem is that the file might still be used by some async void event handler of my form, resulting in an IOException. The code that deletes the file is placed immediately after the Application.Run(form) call inside the Main method. My question is: how can I force the Application.Run method to keep blocking the Main execution flow, until all async void event handlers of my form are completed?

Here is a minimal demonstration of the desirable behavior:

[STAThread] static void Main() { ApplicationConfiguration.Initialize(); int completedOperations = 0; Form form = new(); form.Load += async (s, e) => { await Task.Delay(1000); completedOperations++; }; form.Shown += async (s, e) => { await Task.Delay(2000); completedOperations++; }; Application.Run(form); MessageBox.Show($"Completed operations: {completedOperations}"); } 

The desirable output is:

Completed operations: 2 

Both async void operations should always be completed when the execution flow reaches the final MessageBox.Show line. The actual (undesirable) output of the above code depends on how quickly the user will close the window. If she closes it in less than two seconds, only 0 or 1 operations might be completed.

Clarifications: Configuring the Task.Delay(1000) with .ConfigureAwait(false) is not an option. My actual event handlers interact with UI components, so it's mandatory to resume on the UI thread after the await. The code below the Application.Run(form) should also run on the UI thread. I don't want to introduce multithreading in my application.

Edit: I am looking for the "correct" way to solve this problem. I am not looking for a dirty workaround. I could probably solve my specific problem of the IOException by doing a busy loop after the Application.Run:

while (true) { try { File.Delete(path); break; } catch { } Application.DoEvents(); } 

I am looking for something better/cleaner/less-smelly than this.

17
  • @PoulBak this idea sounds workable, but ideally I would prefer to keep my event handlers clean from any additional infrastructure. Commented Nov 16 at 23:41
  • Add a producer / consumer queue for processing these events? With appropriate dispose & lifetime handling? If you need to manage the lifetime, you need to do this somewhere. Commented Nov 16 at 23:49
  • Well, async void is fire and forget, you need some way to keep track of what's going on. Commented Nov 16 at 23:51
  • @JeremyLakeman I don't think that I can use the producer/consumer pattern, because my event handlers are all running on the UI thread. But if you have a specific idea of how to make this work, you could consider posting an answer. Commented Nov 16 at 23:53
  • 1
    @PoulBak although the SemaphoreSlim idea is not ideal, feel free to post an answer that shows a solution to the minimal problem in my question, if you want. Commented Nov 17 at 0:17

4 Answers 4

6

This isn't possible without serious hacks. The cleanest solution would be to remove the async void methods and keep a collection of Tasks or whatnot.

async void methods do notify their synchronization context of operations starting and ending (via OperationStarted and OperationCompleted). Old-school (.NET Framework) ASP.NET used this to detect when async void WebForms event handlers completed so they knew when they could send the response. WindowsFormsSynchronizationContext does not override these methods, so they are not used in WinForms (the base class implementations are noops). Normally I would say you can define your own SynchronizationContext wrapping the WindowsFormsSynchronizationContext and providing an implementation of OperationStarted and OperationCompleted. However, this will not work for Windows Forms because there's large portions of that framework that specifically require a WindowsFormsSynchronizationContext and will just silently fail if it's any other form of SynchronizationContext. And WindowsFormsSynchronizationContext, naturally, is sealed.

So, you're left with a more serious hack: use a dynamic runtime hooking library (like Harmony) to intercept calls to SynchronizationContext.OperationStarted and SynchronizationContext.OperationCompleted, and use that to keep a count. That maybe might work. I've used Harmony, but never for anything that crazy; good luck!

Sign up to request clarification or add additional context in comments.

10 Comments

Thanks Mr Cleary for the answer. Could you give an example of a Windows Forms control that doesn't work unless the WindowsFormsSynchronizationContext is present? In my application I am using controls from the standard .NET toolbox, nothing fancy or 3rd party.
I don't remember, sorry. But I definitely remember running into this when trying something similar - using just stock .NET WinForms components. That was several years ago, but with WinForms in maintenance mode I would not expect it to be fixed. If you do try the wrapping-SyncCtx approach, let me know if it's successful!
IIRC, the behavior I observed is that an async method would just fail to resume execution, as though the await never completed.
Thanks, I'll give it a try. My form has basically only a Label and a ProgressBar, so if it works there it won't be a definitive answer for the general viability of this solution. :-)
Btw I've already tried removing the async void methods and keeping a collection of Tasks, but then I stumbled with the problem of where to await those tasks. I can't await them after the Application.Run because the Main is not async. If I make it async then the [STAThread] isn't respected. If I block the thread with Wait or GetAwaiter().GetResult() then the application deadlocks.
That is one case where Task.Run(() => Task.WaitAll(collection)).GetAwaiter().GetResult() would be acceptable.
Hmm, where would I put it though? If I put it after the Application.Run it'll block the UI thread, and the tasks in the collection need the UI thread to complete.
Ah, gotcha. Yeah, if they do need the UI thread, then they need to complete before Run exits. (Otherwise, nothing is pumping that message loop).
Btw I am surprised that a question like this wasn't asked 10-12 years ago. Waiting for your handlers to finish before closing your program sounds like the most basic thing to ask from a UI framework. I could bet that there are countless applications out there that are closing in the middle of ongoing background work, unknowingly to both the user of the program, and the programmer who wrote it.
I've been putting together .NET host support for UI frameworks (github.com/StephenCleary/Hosting) but haven't added support for WinForms (yet). Using hosting, you could do a background worker that didn't quit until its queue was complete. It wouldn't support async void out of the box, though; background work would have to be explicitly queued.
5

Use TaskCompletionSource

 static void Main() { ApplicationConfiguration.Initialize(); int completedOperations = 0; var allDoneTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); void OperationCompleted() { if (Interlocked.Increment(ref completedOperations) == 2) { allDoneTcs.TrySetResult(true); } } using var form = new Form(); form.Load += async (s, e) => { await Task.Delay(1000).ConfigureAwait(false); OperationCompleted(); }; form.Shown += async (s, e) => { await Task.Delay(2000).ConfigureAwait(false); OperationCompleted(); }; Application.Run(form); if (completedOperations < 2) { allDoneTcs.Task.GetAwaiter().GetResult(); } MessageBox.Show($"Completed operations: {completedOperations}"); } 

If you can afford to have form visible then use FormClosing and cancel until completed :

static void Main() { ApplicationConfiguration.Initialize(); int completedOperations = 0; var allDone = new TaskCompletionSource<bool>(); void OperationCompleted() { completedOperations++; if (completedOperations == 2) allDone.TrySetResult(true); } using var form = new Form(); form.Load += async (s, e) => { await Task.Delay(1000); OperationCompleted(); }; form.Shown += async (s, e) => { await Task.Delay(2000); OperationCompleted(); }; bool reallyClosing = false; // Don't let the form close until both async handlers have finished form.FormClosing += async (s, e) => { if (reallyClosing) return; // second time, just let it close if (completedOperations < 2) { e.Cancel = true; // keep Application.Run(form) alive await allDone.Task; // wait until completedOperations == 2 reallyClosing = true; form.Close(); // now close for real } }; Application.Run(form); // Here both async operations are guaranteed completed and the form is closed MessageBox.Show($"Completed operations: {completedOperations}"); } 

13 Comments

Thanks ClearLogic for the answer. You added .ConfigureAwait(false); to make it work. Without it the application deadlocks. Unfortunately my actual event handlers interact with UI components, so it's mandatory to resume on the UI thread after the await. I'll update the question to clarify this detail. Nevertheless +1 for the effort. :-)
The idea of cancelling the FormClosing event and hiding the form instead of closing it passed through my mind, at which point I decided to ask this question hoping that a simpler and less risky solution might exist. Having to keep track how many async void operations have started, including all the try/finally blocks that I'll have to add in every event handler, sounds like a nightmare (considering that my event handlers are already quite complex and indented).
Well, that "nightmare" sounds to me as a simple wrapper method that takes an action argument and does all those boilerplate code. And I would not track this async void but have a collection where I add all tasks I need to track. Side effect: get rid of a lot of async voids.
If any task is created due to user interaction it might be useful to show a modal dialog while waiting for all tasks to be completed. So that the user does not start pressing buttons and add new tasks while the application is in the process of shutting down.
@SirRufo feel free to post an answer that demonstrates this wrapper (hint, not all events have the same signature), or demonstrates the list of tasks instead of async voids. I want to see where you will await those tasks (hint, the Main is not async, and can't be made async).
Well I reacted to your comment with the FormClosing and you argued against it because of all the tracking and wrapping and I gave you a hint how to go around that so called "nightmare"
@JonasH I don't want to adapt the UI experience of the user to the technicalities of the async/await technology. From a UI perspective there is no reason to show a modal dialog. The user has clicked the Cancel button, and they expect the program to close without further UI interaction. The fact that I have to delete a temp file before terminating the process is none of user's business.
I'm not suggesting any interaction. Just a "shutting down..."-message that disappears automatically once everything is finished. The important part is to prevent input, and there are other ways to achieve that if you prefer something else. But if there is any risk that you need to wait for more than a second or so it can be very useful to display something like "Shutting down, saving foo", so if something takes abnormally long time the user can report something meaningful. You might also want to report errors that occur during shutdown.
I go with you and the user should be informed why the app he wants to shut down now is still alive. If he knows the app is shutting down he maybe do not kill the process
|
2

Since what causes Application.Run to return is the main form closing, the most simple not hacky way to do it for me seems to prevent the form from closing (duh).

For example with something like this:

internal static class Program { [STAThread] static void Main() { ApplicationConfiguration.Initialize(); var completedOperations = 0; var form = new Form(); form.FormClosing += EarlyClosingBlocker.HandleFormClosing; form.Load += (s, e) => _ = EarlyClosingBlocker.Run(async () => { await Task.Delay(1000); completedOperations++; }); form.Shown += (s, e) => _ = EarlyClosingBlocker.Run(async () => { await Task.Delay(2000); completedOperations++; }); Application.Run(form); MessageBox.Show($"Completed operations: {completedOperations}"); } } public static class EarlyClosingBlocker { private static readonly ConcurrentDictionary<AutoResetEvent, object?> _waitEvents = new(); public static async Task Run(Func<Task> action) { var are = new AutoResetEvent(false); _waitEvents[are] = null; try { await action(); } finally { are.Set(); _waitEvents.TryRemove(are, out _); } } public static void HandleFormClosing(object? sender, FormClosingEventArgs e) { var events = _waitEvents.ToArray(); if (events.Length == 0) return; // note we could use e.CloseReason to decide not to wait whatsoever e.Cancel = true; var form = (Form)sender!; form.Hide(); // or put a message, etc. _ = Task.Run(() => { // wait for all events to finish, note this cannot run in an STA thread WaitHandle.WaitAll([.. events.Select(e => e.Key)]); form.BeginInvoke(form.Close); }); } } 

1 Comment

Thanks Simon for the answer. It works like a charm. Instead of form.Load += (s, e) => _ = EarlyClosingBlocker.Run( I would prefer form.Load += async (s, e) => await EarlyClosingBlocker.Run(, so that any exception in the event handler is propagated as usual (instead of being suppressed).
1

I implemented Stephen Cleary's hesitant advice:

Normally I would say you can define your own SynchronizationContext, wrapping the WindowsFormsSynchronizationContext and providing an implementation of OperationStarted and OperationCompleted.

The AsyncVoidAwareContext class below is such wrapper. It can be used by replacing the line Application.Run(form); with AsyncVoidAwareContext.Run(form);. Nothing else has to be changed. The AsyncVoidAwareContext.Run keeps the current execution flow blocked until the form is closed, and all async void operations are completed. So I can safely delete my temp file immediately after that, because the file is not in use by any async void operation. Here is the implementation:

/// <summary> /// WindowsFormsSynchronizationContext wrapper. /// </summary> public class AsyncVoidAwareContext : SynchronizationContext { /// <summary> /// Begins running a standard application message loop on the current thread, /// makes the specified form visible, and exits when the form is closed and all /// async void operations are completed. /// </summary> public static void Run(Form form) { ArgumentNullException.ThrowIfNull(form); SynchronizationContext original = SynchronizationContext.Current; AsyncVoidAwareContext sc = new(original); SynchronizationContext.SetSynchronizationContext(sc); try { form.FormClosed += Form_FormClosed; form.Show(); Application.Run(); } finally { if (ReferenceEquals(SynchronizationContext.Current, sc)) SynchronizationContext.SetSynchronizationContext(original); } void Form_FormClosed(object sender, FormClosedEventArgs e) { form.FormClosed -= Form_FormClosed; sc.OperationCompleted(); } } private readonly SynchronizationContext _original; private int _pendingOperations = 1; public AsyncVoidAwareContext(SynchronizationContext original) => _original = original ?? new SynchronizationContext(); public override SynchronizationContext CreateCopy() => new AsyncVoidAwareContext(_original); public override void OperationStarted() => Interlocked.Increment(ref _pendingOperations); public override void OperationCompleted() { if (Interlocked.Decrement(ref _pendingOperations) == 0) _original.Post(static _ => Application.ExitThread(), null); } public override void Post(SendOrPostCallback d, object state) => _original.Post(d, state); public override void Send(SendOrPostCallback d, object state) => _original.Send(d, state); } 

This implementation works in my project like a charm. I should mention that my project uses very few UI components, namely a Label, a ProressBar and a Button, and Stephen Cleary in his answer has warned about potential incompatibilities of stock .NET WinForms components with non-standard synchronization contexts. So use this solution with caution and at your own risk.

This implementation works even in case of .ConfigureAwait(false) inside the handlers. The AsyncVoidAwareContext.Run(form) blocks until all handlers are completed, even if they have exited the UI context and are running on other threads.

A small technical detail is that the WindowsFormsSynchronizationContext uninstalls and disposes itself after the completion of the message loop. On the contrary the implementation above reinstalls the captured WindowsFormsSynchronizationContext after the completion of the message loop. I didn't find a way to uninstall and dispose it, other than starting and exiting a second (subsequent) message loop. Most applications exit immediately after the Application.Run(form), so this minor flaw shouldn't be a problem.

For anyone interested at what point the WindowsFormsSynchronizationContext is installed in the first place, it happens during the construction of the Form class. The Control class and any component deriving from it, installs automatically a WindowsFormsSynchronizationContext on the current thread during its construction, if one is not installed already (source code).

2 Comments

FWIW, bit late in the game, but I would use FormClosing, it seems the more easy and natural way, for example pastebin.com/raw/GZQbdd0s
@SimonMourier you can still post an answer. ClearLogic presented a similar idea in the second part of his answer (hiding the form instead of closing it), but in his answer he tried to solve just the minimal example in the question, without offering a general solution for an unknown number of async void operations.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.