Is it possible to use ShowDialog without blocking all forms?

I hope I can explain this clearly enough. I have my main form (A) and it opens 1 child form (B) using form.Show() and a second child form (C) using form.Show(). Now I want child form B to open a form (D) using form.ShowDialog(). When I do this, it blocks form A and form C as well. Is there a way to open a modal dialog and only have it block the form that opened it?


Solution 1:

Using multiple GUI threads is tricky business, and I would advise against it, if this is your only motivation for doing so.

A much more suitable approach is to use Show() instead of ShowDialog(), and disable the owner form until the popup form returns. There are just four considerations:

  1. When ShowDialog(owner) is used, the popup form stays on top of its owner. The same is true when you use Show(owner). Alternatively, you can set the Owner property explicitly, with the same effect.

  2. If you set the owner form's Enabled property to false, the form shows a disabled state (child controls are "grayed out"), whereas when ShowDialog is used, the owner form still gets disabled, but doesn't show a disabled state.

    When you call ShowDialog, the owner form gets disabled in Win32 code—its WS_DISABLED style bit gets set. This causes it to lose the ability to gain the focus and to "ding" when clicked, but doesn't make it draw itself gray.

    When you set a form's Enabled property to false, an additional flag is set (in the framework, not the underlying Win32 subsystem) that certain controls check when they draw themselves. This flag is what tells controls to draw themselves in a disabled state.

    So to emulate what would happen with ShowDialog, we should set the native WS_DISABLED style bit directly, instead of setting the form's Enabled property to false. This is accomplished with a tiny bit of interop:

    const int GWL_STYLE   = -16;
    const int WS_DISABLED = 0x08000000;
    
    [DllImport("user32.dll")]
    static extern int GetWindowLong(IntPtr hWnd, int nIndex);
    
    [DllImport("user32.dll")]
    static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
    
    void SetNativeEnabled(bool enabled){
        SetWindowLong(Handle, GWL_STYLE, GetWindowLong(Handle, GWL_STYLE) &
            ~WS_DISABLED | (enabled ? 0 : WS_DISABLED));
    }
    
  3. The ShowDialog() call doesn't return until the dialog is dismissed. This is handy, because you can suspend the logic in your owner form until the dialog has done its business. The Show() call, necessarily, does not behave this way. Therefore, if you're going to use Show() instead of ShowDialog(), you'll need to break your logic into two parts. The code that should run after the dialog is dismissed (which would include re-enabling the owner form), should be run by a Closed event handler.

  4. When a form is shown as a dialog, setting its DialogResult property automatically closes it. This property gets set whenever a button with a DialogResult property other than None is clicked. A form shown with Show will not automatically close like this, so we must explicitly close it when one of its dismissal buttons is clicked. Note, however, that the DialogResult property still gets set appropriately by the button.

Implementing these four things, your code becomes something like:

class FormB : Form{
    void Foo(){
        SetNativeEnabled(false); // defined above
        FormD f = new FormD();
        f.Closed += (s, e)=>{
            switch(f.DialogResult){
            case DialogResult.OK:
                // Do OK logic
                break;
            case DialogResult.Cancel:
                // Do Cancel logic
                break;
            }
            SetNativeEnabled(true);
        };
        f.Show(this);
        // function Foo returns now, as soon as FormD is shown
    }
}

class FormD : Form{
    public FormD(){
        Button btnOK       = new Button();
        btnOK.DialogResult = DialogResult.OK;
        btnOK.Text         = "OK";
        btnOK.Click       += (s, e)=>Close();
        btnOK.Parent       = this;

        Button btnCancel       = new Button();
        btnCancel.DialogResult = DialogResult.Cancel;
        btnCancel.Text         = "Cancel";
        btnCancel.Click       += (s, e)=>Close();
        btnCancel.Parent       = this;

        AcceptButton = btnOK;
        CancelButton = btnCancel;
    }
}

Solution 2:

You can use a separate thread (as below), but this is getting into dangerous territory - you should only go near this option if you understand the implications of threading (synchronization, cross-thread access, etc.):

[STAThread]
static void Main() {
    Application.EnableVisualStyles();
    Button loadB, loadC;
    Form formA = new Form {
        Text = "Form A",
        Controls = {
            (loadC = new Button { Text = "Load C", Dock = DockStyle.Top}),
            (loadB = new Button { Text = "Load B", Dock = DockStyle.Top})
        }
    };
    loadC.Click += delegate {
        Form formC = new Form { Text = "Form C" };
        formC.Show(formA);
    };
    loadB.Click += delegate {
        Thread thread = new Thread(() => {
            Button loadD;
            Form formB = new Form {
                Text = "Form B",
                Controls = {
                    (loadD = new Button { Text = "Load D",
                        Dock = DockStyle.Top})
                }
            };
            loadD.Click += delegate {
                Form formD = new Form { Text = "Form D"};
                formD.ShowDialog(formB);
            };
            formB.ShowDialog();  // No owner; ShowDialog to prevent exit
        });
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
    };
    Application.Run(formA);
}

(Obviously, you wouldn't actually structure the code like the above - this is just the shortest way of showing the behavior; in real code you'd have a class per form, etc.)

Solution 3:

If you run Form B on a separate thread from A and C, the ShowDialog call will only block that thread. Clearly, that's not a trivial investment of work of course.

You can have the dialog not block any threads at all by simply running Form D's ShowDialog call on a separate thread. This requires the same kind of work, but much less of it, as you'll only have one form running off of your app's main thread.

Solution 4:

I would like to summarize possible solutions and add one new alternatives (3a and 3b). But first I want to clarify what we are talking about:

We have an application which have multiple forms. There is a requirement to show modal dialog which would block only certain subset of our forms but not the others. Modal dialogs may be shown only in one subset (scenario A) or multiple subsets (scenario B).

And now summary of possible solutions:

  1. Don't use modal forms shown via ShowDialog() at all

    Think about design of your application. Do you really need to use ShowDialog() method? If you don't need having modal form it's the easiest and the cleanest way to go.

    Of course this solution is not always suitable. There are some features which ShowDialog() gives us. The most notable is that it disables the owner (but do not grays out) and user cannot interact with it. The very exhausting answer provided P Daddy.

  2. Emulate ShowDialog() behavior

    It is possible to emulate behavior of that mathod. Again I recommend reading P Daddy's answer.

    a) Use combination of Enabled property on Form and showing form as non-modal via Show(). As a result disabled form will be grayed out. But it's completely managed solution without any interop needed.

    b) Don't like the parent form being grayed out? Reference few native methods and turn off WS_DISABLED bit on parent form (once again - see answer from P Daddy).

    Those two solutions require that you have complete control on all the dialog boxes you need to handle. You have to use special construct to show "partially blocking dialog" and must not forget about it. You need to adjust your logic because Show() is non-blocking and ShowDialog() is blocking. Dealing with system dialogs (file choosers, color pickers, etc.) could be problem. On the other hand you do not need any extra code on the forms which shall not be blocked by dialog.

  3. Overcome limitations of ShowDialog()

    Note that there are Application.EnterThreadModal and Application.LeaveThreadModal events. This event is raised whenever modal dialog is shown. Beware that events are actually thread-wide, not application-wide.

    a) Listen to the Application.EnterThreadModal event in forms which shall not be blocked by dialog and turn on WS_DISABLED bit in those forms. You only need to adjust forms which should not be blocked by modal dialogs. You may also need to inspect the parent-chain of the modal form being shown and switch WS_DISABLED based on this condition (in your example if you also needed to open dialogs by forms A and C but not to block forms B and D).

    b) Hide and re-show forms which should not be blocked. Note that when you show new form after modal dialog is shown it is not blocked. Take advantage of that and when modal dialog is shown, hide and show again desired forms so that they are not blocked. However this approach may bring some flickering. It could be theoretically fixed by enabling/disabling repaint of forms in Win API but I do not guarantee that.

    c) Set Owner property to dialog form on forms which should not be blocked when dialog is shown. I did not test this.

    d) Use multiple GUI threads. Answer from TheSmurf.