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:
When
ShowDialog(owner)
is used, the popup form stays on top of its owner. The same is true when you useShow(owner)
. Alternatively, you can set theOwner
property explicitly, with the same effect.-
If you set the owner form's
Enabled
property tofalse
, the form shows a disabled state (child controls are "grayed out"), whereas whenShowDialog
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—itsWS_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 tofalse
, 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 nativeWS_DISABLED
style bit directly, instead of setting the form'sEnabled
property tofalse
. 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)); }
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. TheShow()
call, necessarily, does not behave this way. Therefore, if you're going to useShow()
instead ofShowDialog()
, 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 aClosed
event handler.When a form is shown as a dialog, setting its
DialogResult
property automatically closes it. This property gets set whenever a button with aDialogResult
property other thanNone
is clicked. A form shown withShow
will not automatically close like this, so we must explicitly close it when one of its dismissal buttons is clicked. Note, however, that theDialogResult
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:
-
Don't use modal forms shown via
ShowDialog()
at allThink 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. -
Emulate
ShowDialog()
behaviorIt is possible to emulate behavior of that mathod. Again I recommend reading P Daddy's answer.
a) Use combination of
Enabled
property onForm
and showing form as non-modal viaShow()
. 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 andShowDialog()
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. -
Overcome limitations of
ShowDialog()
Note that there are
Application.EnterThreadModal
andApplication.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 onWS_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 switchWS_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.