MDI Essentials
In .NET, there is no sharp distinction between ordinary windows and MDI windows. In fact, you can transform any window into an MDI parent at design-time or runtime by setting the IsMdiContainer container. You can even change a window back and forth at will, which is a mind-expanding trick never before allowed.
this.IsMdiContainer = true;
When displayed as an MDI container, the form's surface becomes a dark gray open area where other windows can be hosted. To add a window as an MDI child, you simply set the form's MdiParent property on startup:
Child frmChild = new Child();
frmChild.MdiParent = this;
frmChild.Show();
Ideally, you perform this task before you display the window, but with .NET you don't need to. In fact, you can even have more than one MDI parent in the same project, and move a child from one parent to the other by changing the MdiParent property.Figure 10-1 shows two different views of an MDI parent with a contained MDI child.

Figure 10-1: An MDI Child
One of the most unusual features of .NET MDI parents is that they can display any type of control. Traditionally, MDI parents only support docked controls like toolbars, status bars, and menus. With an MDI parent created in .NET, however, you can add any other type of control, and it remains fixed in place (or anchored and docked), suspended "above" any other windows.This trick can be used to create a bizarre window like that shown in Figure 10-2, or a unique type of floating tool window (although you need to add the "fake" drag-and-drop support, as described in Chapter 4).

Figure 10-2: Suspended controls
Tip
MDI child forms can be minimized or maximized. When maximized, they take up the entire viewable area, and the title name appears in square brackets in the MDI container's title bar. When minimized, just the title bar portion appears at the bottom of the window. You can prevent this behavior by disabling the Show Maximize or ShowMinimize properties for the child form.
Finding Your Relatives
If you display multiple windows in an SDI application, you need to carefully keep track of each one, usually by storing a form reference in some sort of static application class. With MDI interfaces, you don't need to go to this extra work. That's because it's easy to find the currently active MDI window, the MDI parent, and the full collection of MDI children.Consider the next example, which provides a toolbar with two buttons: New and Close. The New button creates an MDI child window, while the Close button always closes the currently active window (see Figure 10-3). You don't need to write any extra code to track the currently active child. Instead, it is provided through the MDI container's ActiveMdiChild property.

Figure 10-3: Working with the active child
Here's the code that handles the ToolBar.ButtonClick event, which fires when either the New or Close button is clicked:
private void toolBar1_ButtonClick(object sender,
System.Windows.Forms.ToolBarButtonClickEventArgs e)
{
// Determine which button was clicked.
if (e.Button == cmdNew)
{
// Show a new ChildForm.
Child frmChild = new Child();
frmChild.MdiParent = this;
frmChild.Show();
}
else if (e.Button == cmdClose)
{
// Close the active child.
this.ActiveMdiChild.Close();
}
}
Tip
You can also set the active MDI form using the Form.Activate() method. This is similar to setting the focus for a control. It automatically moves the appropriate child form to the top of all other child forms, and sets the focus to the most recently selected control on that form. You can also find the control that has focus on an MDI form by reading the ActiveControl property.
Synchronizing MDI Children
The MdiParent property allows you to find the MDI container from any child. The ActiveMdiChild property allows you to find the active child from the parent form. The only remaining gap to fill is retrieving the full list of all MDI children. This can be accomplished using the MdiChildren property, which provides an array of form references. (That's right, an array—not a collection, which means you can't use methods like Add() and Remove() to manage MDI children.)The next example shows how you can use the MdiChildren array to synchronize MDI children. In this example, every child shows a text box with the same content. If the text box content is modified in one window, the custom RefreshChildren() method is called in the parent form.
private bool isUpdating;
// Triggered in response to the TextBox1.TextChanged event.
private void textBox1_TextChanged(object sender, System.EventArgs e)
{
if (this.MdiParent != null && !isUpdating)
{
// The reference to the MDI parent must be converted to the appropriate
// form class in order to access the custom RefreshChildren() method.
((Parent)this.MdiParent).RefreshChildren(this, textBox1.Text);
}
}
The RefreshChildren() method in the MDI parent form steps through all the child windows, and updates each one, except the original sender. It also stores the current text in a private member variable, so it can assign it automatically to newly created windows.
private string synchronizedText;
public void RefreshChildren(Child sender, string text)
{
// Store text for use when creating a child form, or if needed later.
synchronizedText = text;
// Update children.
foreach (Child frm in this.MdiChildren)
{
if (frm != sender)
{
frm.RefreshText(text);
}
}
}
The refreshing is performed through the RefreshText() method provided by each child window. It takes special care to avoid triggering another refresh by disabling the event handler for the duration of the task.
public void RefreshText(string text)
{
// Disable the event to prevent an endless string of updates.
isUpdating = true;
// Update the control.
textBox1.Text = text;
// Re-enable the event handler.
isUpdating = false;
}
This example shows how synchronization can be implemented using the MdiChildren property. However, the potential drawback of this technique is that it forces every window to be updated even if the change only affects one or two. This is suitable if all windows are linked together, but is not useful if the user is working in multiple independent windows. A more scalable approach is introduced later when you explore document-view architecture.
MDI Layout
By convention, MDI applications often provide a menu that lists all the open document windows, and provides options for automatically tiling or cascading them. Adding these features in .NET is easy.To create an MDI child window list, simply add a top-level menu item (usually named Window), and set the MdiList property to true. The Windows Forms engine will then automatically add one item to the bottom of the submenu for each child window (using the title bar for the menu text), and place a check mark next to the window that is currently active (see Figure 10-4). The user can also use the menu to move from window to window, without any required code.

Figure 10-4: The MDI child list
If you want to add the support for tiling and cascading windows, you'll probably also add these options to this menu. Every MDI container supports a LayoutMdi() method that accepts a value from the MdiLayout enumeration, and arranges the windows automatically.For example, here's the code to tile windows horizontally in response to a menu click:
private void mnuTileH_Click(object sender, System.EventArgs e)
{
this.LayoutMdi(MdiLayout.TileHorizontal);
}
Of course, it's just as easy to create your own custom layout logic. Here's the code for a menu option that minimizes all the open windows:
private void mnuMinimizeAll_Click(object sender, System.EventArgs e)
{
foreach (Form frm in this.MdiChildren)
{
frm.WindowState = FormWindowState.Minimized;
}
}
Figure 10-5 summarizes some of the layout options.

Figure 10-5: Different layout options
Merging Menus
Another unique characteristic of MDI applications is their treatment of menus. If you create a child form with a menu, that menu is added to the main menu when the child form is displayed. This behavior allows you to provide different options depending on the current view, but presents a centralized menu to the user.Using the default menu behavior, menu items from the child form are added to the right of the standard menu items. Figure 10-6 shows an example with a child menu named Document. However, you can configure this behavior to a certain extent using the MergeStyle and MergeOrder properties.

Figure 10-6: Merged menus
To add a child menu in front of a parent menu, set the MergeOrder for the child menu to 0 (the default) and change the MergeOrder in the parent menu items to 1, or any larger number.
To ensure that a child menu does not appear when displayed in an MDI container, set the MergeStyle for the menu item to Remove.
To merge similar menus together (for example, the entries under two toplevel File menus), make sure that the MergeOrder for both is the same, and set the MergeStyle of each one to MergeItems. Without this step, you would end up with two identically named items, one with the child menu items and one with the parent items.
Note
.NET does not automatically merge toolbars (or any other type of control you dock to the MDI parent form). If this is the behavior you want, you will have to write the code to perform this task manually.
Managing Interface State
When creating MDI applications, you'll often find that you have more than one control with equivalent functionality. The most common example is the toolbar, which usually replicates options in the main menu.You can handle this duplication fairly easily in code. One technique is to hand off the work to another method. Thus, both the toolbar button-click and the menu-click event handler forward requests for a new document to a form-level or application class method like NewDocument(). Here's how it works:
public class MDIParent : System.Windows.Forms.Form
{
// (Designer code omitted.)
private void toolBar1_ButtonClick(object sender,
System.Windows.Forms.ToolBarButtonClickEventArgs e)
{
if (e.Button == cmdOpen)
{
ApplicationTasks.NewDocument(this);
}
}
private void mnuNew_Click(object sender, System.EventArgs e)
{
ApplicationTasks.NewDocument(this);
}
}
public class ApplicationTasks
{
public static void NewDocument(Form parentForm)
{
// (Code implementation here.)
}
}
Life becomes a little trickier when you need to handle the enabled/disabled state for these controls. For example, rather than performing error checking to verify there is an active document when the user clicks Save, you should disable the Save button and menu option unless a document is available. The problem is that you not only have to disable the menu option, but you need to ensure that the corresponding toolbar button (or any other control that provides the same functionality) becomes disabled or enabled at the same time. Otherwise, mysterious bugs can creep into your application, where controls allow a function to be attempted when the document is in an invalid state. If you are performing all of your testing with the menu bar, you might not even notice this vulnerability, because it's exposed solely through the toolbar.Generally, you'll need a dedicated controller class (often called a state management class) to assume this responsibility. One option is to provide higher-level methods or properties in the controller class that automatically disable or enable related controls. Then your code will call one of these methods instead of manually interacting with the appropriate controls.Here's how a controller class like this might look:
public class MDIMainController
{
public Form MDIMain;
public bool NewEnabled
{
get
{
return MDIMain.mnuNew.Enabled;
}
set
{
MDIMain.mnuNew.Enabled = value;
MDIMain.cmdNew.Enabled = value;
}
}
}
This is typical of many programming solutions: it works by adding another layer of indirection. The MDIMainController acts as a layer between the form and the user interface code. When you want to remove the ability for the user to create new documents, you simply use a single line of code:
ControllerInstance.NewEnabled = false;
As with many programming tasks, the trick is in managing the details. The controller class technique works well and helps tame the inevitable complexity of an interface. However, you need to design with this technique in mind from the beginning, even if the interface only exposes a few simple options.