+7  A: 

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.

DannySmurf
I thought all of the UI had to run on the same thread?
Jon Tackabury
Jon T - It doesn't have to, but you'd better know what you're doing and hope that there's no interaction between the 2 - sounds like trouble to me.
Sam Meldrum
I know about invoking between threads and UI threads, I've done quite a bit of that. I was just hoping there was a way to avoid using threading to solve this. It looks like threading might be the only answer though.
Jon Tackabury
It certainly is trouble, if you don't know what you're doing. But assuming the two don't communicate (or the communication is as simple as reading some data from form D after it closes), it's trivial to implement.
DannySmurf
All UI does not have to run on the same thread, you just need to be sure that you access a form's UI properties on that form's UI thread. So, if A and B are running in different threads, if A accesses B, it will need to do an Invoke. In our app, all forms run on their own threads.
Rob Prouse
This is some great information. I knew that you had to use invoke to access UI elements, but I was under the wrong impression that you also had to keep all of those UI elements on the same thread. Running it on a separate thread seems to be the best way to go.
Jon Tackabury
It should be noted that, while you may be comfortable with multiple UI threads, later maintainers may not be. Putting this on another UI thread is building in some expensive maintenance if you're not the only dev.
Greg D
@Greg D: We're not talking about writing an OS scheduler here; what one needs to know to do this is probably the minimal amount every desktop app programmer should already know about threading.
DannySmurf
A: 

You can have access to other windows while a modal is open in an MDI application, still any form in the chain that opened the modal would still be inaccessible.

cmsjr
+1  A: 

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)

Marc Gravell
Excellent example - thanks. I've decided to go down the threaded road, as it seems like the best option.
Jon Tackabury
A: 

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

Robert Venables
+13  A: 

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;
    }
}
P Daddy
+1 a much more complete answer than mine - but essentially the same approach! Will get rid of mine as yours is so much more complete.
Sam Meldrum