Introduction
Microsoft is trying to kill support for MDI. In certain kinds of applications where many child forms need to be managed independently, it's really necessary to have MDI support. Microsoft is encouraging developers to move to other alternative approaches to MDI: Single Document Interface (SDI), Tabbed Workspaces (like IE7 and Firefox), or the navigational model. Since MDI is discouraged, WPF has no support for this. But in this article, I am going to show how we can use the WPF TabControl
to get the flavor of MDI.
Basic Idea
The way I’m going to present the MDI behavior will not be like in a full-fledged MDI form. But, this concept will allow you to put your content in a multi-tabbed manner in a TabControl
. Your WPF project will have a window. In that window, you’ll add a TabControl
. This TabControl
will host your pages (for this scenario, you need to develop a user control rather than a page) as tab items. This concept is shown in Figure 1.
Figure 1: MDI behavior with Tab control
In this scenario, you’ll create a user control (i.e., the MDI child) for your pages. It’s because you can’t host pages inside the TabControl
’s tab item. Lets say you need to develop a few user controls for a system, say a student management system. The user controls may be ucStudentEnrollment
, ucCourseAssignment
etc. Now, when the user clicks on a menu like “Student Enrollment”, you’ll create an instance of ucStudentEnrollment
, add a tab item to the TabControl
, and then set the tab item’s Content
property to the instance of ucStudentEnrollment
.
Now, we need to have a way to make the window control (the MDI parent) interact with its child user controls (MDI children). Let’s have a scenario. When the user clicks on the close button in a user control (MDI child) which is hosted in a window (MDI parent), the user control (MDI child) needs to be removed from the window (MDI parent). The close event will be fired from the user control (MDI child), but the action will be handled by the window (MDI parent). So, there’s an interaction required between the children (User Controls) and the parent (Window
). Also, the window needs to manage its children, such as keep track of if a child page is already open (here in this example, we’ll assume that a single form can’t be opened multiple times in the parent window).
Implementation Details
Each control has to implement an interface, and the MDI parent will interact with its children using an interface. The interface signature in this example is shown below:
public interface ITabbedMDI
{
event delClosed CloseInitiated;
string UniqueTabName{get;}
string Title { get; }
}
In the above interface, the delClosed
delegate has the following signature:
public delegate void delClosed(ITabbedMDI sender, EventArgs e);
This delegate defines the signature of the close event fired from user controls (MDI children). UniqueTabName
is the unique name for the user control. The MDI parent will keep track of which child is currently open, by adding the unique tab name to a dictionary. So whenever a user tries to add a control, the MDI parent will check the dictionary to see whether the control unique name is already in the dictionary. If it is in the dictionary, the MDI parent will not add that control, rather set focus on the tab item where the control is hosted. The title in the interface is the tab tile.
Since each user control will implement the interface, the MDI parent can easily access the common properties of all user controls (e.g., title, unique name, event etc.) by casting it to the interface. When an MDI child is added to the MDI parent, there are a few things to be done:
- Create a new instance of the user control (for simplicity, assume that the user control is not already added). Since
ucTab1
has implemented the interface ITabbedMDI
, the user control will have two properties and an event will be available to the MDI parent.
- The MDI parent has a dictionary object of opened controls’ unique name/title pair. So, check if the user control is already opened by checking its unique name in that dictionary.
- If a user control exists in the dictionary, then set focus on the tab where the user control is hosted, and return without going to the next steps. If the user control unique name does not exist in the dictionary, then go to the next step.
- Create a new
TabItem
and add that TabItem
to TabControl
.
- Set the
TabItem
’s Name
to the User Control’s UniqueName
and set the TabItem
’s Title
to the User Control’s Header
.
- Add the user control’s unique name and title pair in the dictionary maintained by the MDI parent for keeping track of the children.
Interface Implementation in the User Control (MDI Child)
Each user control will implement a single interface so that the MDI parent can manage these user controls (MDI children) in an unique manner. In a user control, the interface ITabbedMDI
can be implemented as shown below:
#region ITabbedMDI Members
public event delClosed CloseInitiated;
public string UniqueTabName
{
get
{
return "Tab2";
}
}
public string Title
{
get { return "Tab 2 Title"; }
}
#endregion
In the same user control, we can fire an event in the button close event as shown below:
private void btnClose_Click(object sender, RoutedEventArgs e)
{
if (CloseInitiated != null)
{
CloseInitiated(this, new EventArgs());
}
}
Window (MDI Parent)'s Interaction with MDI Children
When the user clicks on a menu item for a child form, the parent window creates a child instance as shown below:
private void mnuTab1_Click(object sender, RoutedEventArgs e)
{
ucTab1 mdiChild = new ucTab1();
AddTab(mdiChild);
}
The AddTab
method will check if the tab is already open by checking the user control’s unique name in the opened children name list. If the tab is already open, then the AddTab
method sets the focus on that tab. If the user control is not found, then the control is added to the tab.
private void AddTab(ITabbedMDI mdiChild)
{
if (_mdiChildren.ContainsKey(mdiChild.UniqueTabName))
{
foreach (object item in tcMdi.Items)
{
TabItem ti = (TabItem)item;
if (ti.Name == mdiChild.UniqueTabName)
{
ti.Focus();
break;
}
}
}
else
{
tcMdi.Visibility = Visibility.Visible;
tcMdi.Width = this.ActualWidth;
tcMdi.Height = this.ActualHeight;
((ITabbedMDI)mdiChild).CloseInitiated += new delClosed(CloseTab);
TabItem ti = new TabItem();
ti.Name = ((ITabbedMDI)mdiChild).UniqueTabName;
ti.Header = ((ITabbedMDI)mdiChild).Title;
ti.Content = mdiChild;
ti.HorizontalContentAlignment = HorizontalAlignment.Stretch;
ti.VerticalContentAlignment = VerticalAlignment.Top;
tcMdi.Items.Add(ti);
tcMdi.SelectedItem = ti;
_mdiChildren.Add(((ITabbedMDI)mdiChild).UniqueTabName,
((ITabbedMDI)mdiChild).Title);
}
}
Conclusion
Although Microsoft is discouraging MDI, we need a way to manage child forms inside a parent. MDI is popular as we can see in IE7 and Firefox. Also, we need to use different threads to mange each child form’s activities so that when the user initiates an action from a child form other children does not become irresponsive.