1

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?

micnil
  • 4,705
  • 2
  • 28
  • 39
  • If you're plan on using this in a *library* of sort, state it clearly, because it's a game changer. – Jimi Dec 12 '19 at 15:53

2 Answers2

1

Would it be an option for you to write a dedicated singleton class ("DialogManager"?) that keeps track of the opended dialogs of your application? This class would store the dialog instances or alternatively at least their number).

In order to open / show a dialog you would then call this DialogManager class instead of doing it directly.

When the number of open dialogs reaches zero, you could throw an event in that class e.g. "AllDialogsClosed".

Then you could subscribe to that "AllDialogsClosed" event in your application in another class and do your logic.

public class DialogManager
{
  private int _numberOfOpenDialogs;
  public event EventHandler AllDialogsClosed;


  public ShowDialog(object dialogInstance)
  {
      _numberOfOpenDialogs++;
  }

  public CloseDialog(object dialogInstance)
  {
      _numberOfOpenDialogs--;

      if (_numberOfOpenDialogs == 0)
      {
          OnAllDialogsClosed();
      }
  }

  protected virtual void OnAllDialogsClosed(EventArgs e)
  {
    EventHandler handler = AllDialogsClosed;
    if (handler != null)
    {
        handler(this, e);
    }
  } 
}
Martin
  • 5,165
  • 1
  • 37
  • 50
  • I think this might be a good solution for some people that might find this question, but this is kind-off what what I meant with "_[...] have to make this implementation independent of the actual dialog [...]_", but I understand it might not have been clear. If this only works if other developers remember to call this class everytime they open a dialog, it is very bug prone in the future. It would also require me to modify the potentially hundreds (or thousands) of places we open modal dialogs. I was hoping there was a way to keep the implementation contained to a single class or function. – micnil Dec 12 '19 at 10:44
1

I'm not sure what's the real requirement behind this, but what you are trying to do in your example could be easily done this way:

private async void Form1_Load(object sender, EventArgs e)
{
    await Task.Run(() => MessageBox.Show("Hi"));
    MessageBox.Show("All dialog closed!");
}

It will show the "Hi" as a non-modal and you have access to main window. It also does not block the UI thread, but waits until the "Hi" dialog close and then run the next line.

If above piece of code is what you are looking for, you can ignore rest of the post; however if you want to read it for learning purpose, it shows How to detect all modal dialogs in the current application and How to wait for all of them to close.

You can create a function to count number of modal windows. I see the following modal windows:

  • Those windows like MessageBox or ColorDialog which have #32770 as window class
  • Those Forms which you have shown using ShowDialog, which have Modal property as true.

To enumerate them, you can get all threads of the current process, and then for each thread, then using EnumThreadWindows get all windows and using GetClassName check if the class is #32770.

Then using Application.OpenForms get the list of forms which their Modal property is true.

delegate bool EnumThreadDelegate(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")]
static extern bool EnumThreadWindows(int dwThreadId, EnumThreadDelegate lpfn, IntPtr lParam);
[DllImport("user32.dll")]
static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

static IEnumerable<IntPtr> GetModalWindowsHandles(int processId)
{
    var handles = new List<IntPtr>();
    foreach (ProcessThread thread in Process.GetProcessById(processId).Threads)
        EnumThreadWindows(thread.Id,
            (hWnd, lParam) =>
            {
                var className = new StringBuilder(256);
                GetClassName(hWnd, className, 256);
                if (className.ToString() == "#32770")
                {
                    handles.Add(hWnd);
                }
                return true;
            }, IntPtr.Zero);
    foreach (Form form in Application.OpenForms)
        form.Invoke(new Action(() =>
        {
            if (form.Modal)
                handles.Add(form.Handle);
        }));
    return handles;
}

Example

The following example, opens multiple modals and waits until all of them are closed:

private async void Form1_Load(object sender, EventArgs e)
{
    Task.Run(() => MessageBox.Show("Hi"));
    Task.Run(() => new ColorDialog().ShowDialog());
    Task.Run(() => new Form().ShowDialog());
    await WaitUntil(() => GetModalWindowsHandles(Process.GetCurrentProcess().Id).Count() == 0);
    MessageBox.Show("All dialog closed!");
}
public async Task WaitUntil(Func<bool> condition, int frequency = 25, int timeout = -1)
{
    var waitTask = Task.Run(async () =>
    {
        while (!condition()) await Task.Delay(frequency);
    });
    if (waitTask != await Task.WhenAny(waitTask,
            Task.Delay(timeout)))
        throw new TimeoutException();
}

WaitUntil has taken from this post to use in this example.

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • I'm also interested to know the real requirement as it looks like an [XY Problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) to me. – Reza Aghaei Dec 12 '19 at 14:00
  • This looks more like the effect was after, but a lot more complicated than I hoped for compared to how we solve this today (see updated Background section). I'm a bit confused that we have to loop over all threads in the process to check for dialogs, I thought only the UI thread could open them... but my understanding of .NET is probably lacking. It might take some time for me to dig into this, but i'll be sure to mark it as correct if I find it's the best answer to the question. Thank you. – micnil Dec 12 '19 at 18:14
  • 1
    "Usually" a Windows Forms application have a single UI thread. But for example in the above example, using `Task.Run` I've created the modal dialog windows in different threads. And thanks for the feedback, no worries, put enough effort to validate all the solutions. – Reza Aghaei Dec 13 '19 at 08:35