Dialogs, multiple forms and MDI

Show versus ShowDialog, ownership and results, passing data between forms without global state, and the MDI pattern that still appears in line-of-business tools.

Show() returns immediately and the form is modeless. ShowDialog() blocks until the form closes and returns a DialogResult. Choosing wrongly is the source of most multiple-form bugs.

using (var dlg = new CustomerEditForm(customerId))
{
    dlg.StartPosition = FormStartPosition.CenterParent;
    if (dlg.ShowDialog(this) == DialogResult.OK)
        ReloadCustomer(dlg.SavedCustomer);
}
  • Pass this as the owner so the dialog stays in front of the parent and centres relative to it.
  • A dialog closes itself by setting DialogResult - set AcceptButton and CancelButton and let WinForms do the plumbing.
  • Setting DialogResult = DialogResult.Cancel on the Cancel button's DialogResult property is enough; no click handler required.
  • Always wrap a modal dialog in using - a dialog holds GDI and native handles that are not collected promptly.
// inside the dialog: expose data, never the controls
public Customer SavedCustomer { get; private set; }

private void OnOk(object sender, EventArgs e)
{
    if (!ValidateInput())
    {
        DialogResult = DialogResult.None;   // keep the dialog open on invalid data
        return;
    }
    SavedCustomer = BuildCustomer();
    DialogResult = DialogResult.OK;         // triggers the close
}

Passing data between forms

MechanismGood forCost
Constructor argumentsRequired input, immutable for the dialog's lifeNone - the clearest option
Public property set before ShowOptional settingsEasy to forget to set
Result property read after ShowDialogReturning a small objectNone - keeps the parent in control
Events on the child formModeless forms notifying a parentMust unsubscribe or you leak the parent
A shared static or singletonAlmost nothingHidden coupling and testability damage
// modeless child reporting back, without a static
public partial class LogWindow : Form
{
    public event Action<string> LineAdded;

    private void OnLine(string text)
    {
        listBox1.Items.Add(text);
        LineAdded?.Invoke(text);
    }
}

// parent
var log = new LogWindow();
log.LineAdded += AppendToSummary;
log.FormClosed += (s, e) => log.LineAdded -= AppendToSummary;  // break the reference
log.Show(this);

For a modeless window, Show(owner) keeps it above the owner without blocking it. If the user may open several copies, keep a reference and bring the existing instance to the front instead of creating duplicates.

MDI parents and children

// parent
IsMdiContainer = true;

var child = new DocumentForm { MdiParent = this, Text = "Report 1" };
child.Show();

// arrange what is open
LayoutMdi(MdiLayout.TileHorizontal);

// track the active document
ActiveMdiChildChanged += (s, e) =>
    _statusLabel.Text = ActiveMdiChild?.Text ?? "No document";
  • MDI gives you free child window management, but the multiple-document look reads as dated; the tabbed alternative is a TabControl plus user controls.
  • Merging a child menu into the parent is automatic when the child has a MainMenuStrip - which surprises people who add both.
  • MdiLayout.Cascade, TileHorizontal and TileVertical are the built-in arrangements; write your own only if you need a specific grid.
💡
For a new line-of-business tool, prefer a single shell window with a TabControl of user controls over MDI. It is easier to test, easier to dock panels around, and does not tie the design to a 1990s desktop metaphor.

FAQ

How do I stop a dialog closing on invalid input?
Set DialogResult = DialogResult.None in the OK handler when validation fails, and set DialogResult in the form's FormClosing event only after the data is committed.
Where should the Save button live?
In the dialog. Let the dialog collect and validate data, return a result object, and have the parent own persistence - that keeps the dialog reusable and the parent's transaction boundary intact.

Menus, toolbars and status bars Validation and user input handling

Last refreshed 2026-09-18.