Is it possible to use ShowDialog without blocking all forms?

Viewed 82340

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?

11 Answers

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;
    }
}

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.)

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.

Start FormB in a new thread in FormA:

        (new System.Threading.Thread(()=> {
            (new FormB()).Show();
        })).Start();

Now, any forms opened in the new thread using ShowDialog() will only block FormB and NOT FormA or FormC

I was facing a similar problem in an application I was writing. My main UI was a form running on the main thread. I had a help dialog that I wanted to run as a modeless dialog. This was easy to implement, even to the point of ensuring that I only ever had one instance of the help dialog running. Unfortunately, any modal dialogs I used caused the help dialog to lose focus as well - when it was while some of these modal dialogs were running that having the help dialog there would be most useful.

Using ideas mentioned here, and in other places, I managed to overcome this bug.

I declared a thread inside my main UI.

Thread helpThread;

The following code deals with the event fired to open the help dialog.

private void Help(object sender, EventArgs e)
{
    //if help dialog is still open then thread is still running
    //if not, we need to recreate the thread and start it again
    if (helpThread.ThreadState != ThreadState.Running)
    {
        helpThread = new Thread(new ThreadStart(startHelpThread));
        helpThread.SetApartmentState(ApartmentState.STA);
        helpThread.Start();
    }
}

void startHelpThread()
{
    using (HelpDialog newHelp = new HelpDialog(resources))
    {
        newHelp.ShowDialog();
    }
}

I also needed the initialization of the thread added into my constructor to make sure that I was not referencing a null object the first time this code is run.

public MainWindow()
{
    ...
    helpThread = new Thread(new ThreadStart(startHelpThread));
    helpThread.SetApartmentState(ApartmentState.STA);
    ...
}

This makes sure that the thread has only one instance at any given time. The thread itself runs the dialog, and stops once the dialog is closed. Since it runs on a separate thread, creating a modal dialog from within the main UI does not cause the help dialog to hang. I did need to add

helpDialog.Abort();

to the form closing event of my main UI to make sure that the help dialog closes when the application is terminated.

I now have a modeless help dialog which is not affected by any modal dialogs spawned from within my main UI, which is exactly what I wanted. This is safe since there is no communication needed between the main UI and the help dialog.

I came to this thread looking to do something similar: have multiple independent parent forms, running in the same process, each with their own modal dialog. An analogy, which I planned to use as proof of it being possible, would be multiple Word documents, each with their own Find-and-Replace window.

I was sure I'd done this before, but upon trying it, well what do you know, even Microsoft Word can't do this. It behaves exactly as the OP described: once you open a modal dialog (like Find and Replace using Ctrl+H), ALL Word documents become blocked and can't be interacted with. Worse still, only clicking the original parent document causes the modal dialog to flash. The rest of them just don't respond, and give no hint as to why they're blocked or what needs to be closed.

This could indeed be a very confusing user experience. I find it surprising that this is the case with Microsoft's own flagship office software (and that I haven't noticed it before). However, it also helped me come to terms with my own application behaving the same way.

Further confirmation comes from the .NET documentation itself, which states that "ShowDialog shows the window, disables all other windows in the application, and returns only when the window is closed."


I realize this isn't technically a "solution". Those who really feel their application needs to support fully-independent parent forms can try the workaround by @PDaddy, which seems very thorough.

However, hopefully this gives a little peace of mind to those who (like me) were just trying to be conscientious with their UI and thought they were doing something wrong. You're not crazy. Apparently, this is just how Windows works.

Related