Background
As part of a state transaction1 between different applications on the client (WCF interprocess messaging), it is important that no application state is modified by the user in the meantime. Today, this "blocking" is done with modal dialogs in two steps. First an "invisible"2 modal dialog is opened for a few hundred milliseconds. This blocks user interaction and also stops execution of the method until the dialog is closed. If the transaction is still not finished after the timeout, we show a visible modal progress dialog instead.
This invisible modal dialog is causing issues for us, which I thought I could solve by simply making this an await Task.Delay(timoutBeforeProgressDialog)
3 + blocking user input with a message filter. I thought this would give the same effect as showing an invisible modal dialog for a short duration of time. This does however not seem to be the case. If during a callback in the transaction we show a message box to the user, asking to save their changes, the await Task.Delay(timoutBeforeProgressDialog)
will just continue after the timout and pop up the progress dialog above the message box, blocking user input. This will not happen with the invisible modal. When the invisible modal is closed by the timout, it will not continue executing until the callbacks modal dialog is closed.
You could argue that we shouldn't do it this way, and the transaction logic should be redesigned. This logic is however very ingrained and widespread across the application, and a therefore a costly refactor. I was hoping to just refactor this invisible modal dialog logic.
I might also add that historically, we did not show an invisible modal in this situation, instead we called Application.DoEvents() in a loop until ready to continue (which gives the same effect). All this was implemented long before async await came to .NET.
1 Transaction includes asking if OK to switch context, and then handling context switching callbacks.
2 Invisible modal: Simply a modal dialog that does not steal focus, has no width or height, and opens off-screen.
3 There is also a cancellation token to stop the delay if the transaction finishes before.
Question
I want to stop execution of a method until all modal dialogs in the application are closed (or rephrased: until the main message loop is pumping). Example of what i'm trying to achieve:
public partial class Form1 :Form
{
public Form1() {
InitializeComponent();
}
private async void button_Click(object sender, EventArgs e) {
this.BeginInvoke(new MethodInvoker(() => {
MessageBox.Show(this, "A modal dialog", "Dialog", MessageBoxButtons.OK, MessageBoxIcon.Information);
}));
await Task.Delay(1000);
// wait for all modals to be closed / the main message loop is running.
await AllModalsClosed();
Console.WriteLine("This should be logged after user presses OK in the dialog shown above.");
}
private async Task AllModalsClosed() {
var allModalsClosedSource = new TaskCompletionSource<bool>();
if (!this.TopLevelControl.CanFocus) {
// Modals are open
Application.LeaveThreadModal += (object s, EventArgs args) => {
allModalsClosedSource.SetResult(true);
};
await allModalsClosedSource.Task;
}
}
}
But I'm not sure if this will be correct in all situations, or if its the best way. I also have to make this implementation independent of the actual dialog, since it might be anywhere in the application. Note that I don't want to block the main thread.
I've also tried researching if there is any way to dispatch with BeginInvoke to the main message loop, if this was possible I could rewrite AllModalsClosed
to:
private async Task AllModalsClosed() {
var allModalsClosedSource = new TaskCompletionSource<bool>();
// Made up BeginInvoke variant.
// Is anything like this possible?
this.BeginInvokeToMainMessageLoop(new MethodInvoker(() => {
allModalsClosedSource.SetResult(true);
}));
await allModalsClosedSource.Task;
}
Alternatively, is there maybe some way to configure a Task to only continue on the main message loop when completed?