(Print this page)

Fun with MDI forms
Published date: Monday, September 1, 2003
On: Moer and Éric Moreau's web site

Remember what MDI means? Multiple document interface.

This month column will talk about many topics all related to MDI forms and their children. Some of the topics include the children collection, options normally found under the Window menu, differences between VB.Net 2002 and 2003 (some bugs fix). I will also show you how to dock a child form. Finally, and probably the coolest feature, I will show you how to load a child form from a different assembly without having the parent knowing anything about it at compile time.

Creating the main MDI form

We will first start by creating the main form and add some controls to allow the navigation between child forms.

Create a new project using the Windows application template. Set the IsMdiContainer property of the default form to true. This will enable children forms to be hosted. Notice that more then one form can have this property set to true in the same project but trying to set a MDIContainer as a child of another form will generate an exception at runtime.

To this main form, add a treeview control and dock it to the left of form (using the Dock property). I use the treeview control instead of a menu to select child only to show you how you can dock child forms. You can easily replace the treeview with some menu items if you prefer this later method. Now add a splitter control that will allow us efficient resizing of the treeview and children forms without writing a single line of code.

Now add two forms (name them frmChild1 and frmChild2) that will become our children forms.

Add this method to fill the treeview. Notice that the Tag property is filled with the name of the child form.

Private Sub FillTreeView()
    Dim nodX As TreeNode
    Dim nodChild As TreeNode

    With tvwMenu
        .BeginUpdate()

        'Add the First Parent
        nodX = New TreeNode("Hardcoded elements")
        nodChild = New TreeNode("Child1")
        nodChild.Tag = "frmChild1"
        nodX.Nodes.Add(nodChild)
        nodChild = New TreeNode("Child2")
        nodChild.Tag = "frmChild2"
        nodX.Nodes.Add(nodChild)
        .Nodes.Add(nodX)
        nodX = Nothing

        'Add the parent for the dynamic elements
        nodX = New TreeNode("Dynamic elements")
        .Nodes.Add(nodX)
        nodX = Nothing

        .EndUpdate()
    End With
End Sub
You also need to call this method from the Load event of your MDI main form. We are now ready to code the display of the child form. We will use the double click event of the treeview to do this.
Private Sub tvwMenu_DoubleClick(ByVal sender As Object, _
                                ByVal e As System.EventArgs) _
                                Handles tvwMenu.DoubleClick
   Select Case DirectCast(tvwMenu.SelectedNode.Tag, String)
       Case String.Empty
           'Do Nothing
       Case "frmChild1"
           Dim frm As New frmChild1()
           With frm
               .MdiParent = Me
               .FormBorderStyle = FormBorderStyle.None
               .Show()
               .Dock = DockStyle.Fill
           End With
       Case "frmChild2"
           Dim frm As New frmChild2()
           With frm
               .MdiParent = Me
               .Show()
               .Dock = DockStyle.Fill
           End With
       Case Else
           MessageBox.Show("Houston, we have a problem!")
   End Select
End Sub
If we look at what we did here, we see that we are testing the Tag property of the selected node of the treeview (because this property is filled with the name of the form to display). When the tag is filled, its value is compared against some values (frmChild1 and frmChild2). In these cases, we create a new instance of the child form, we set its MdiParent property to the instance of the main form (Me) and we show the child. One neat feature is the Dock property that will dock the child in the space that remains empty to the right of the splitter automatically. Again, no code is required. Another property that can be nice is the FormBorderStyle. If you look at the code to display the first child form, you will see it is set to none. This means that no border (no box and no title bar) will surround your form, which looks like a single form design! One small caveat here, the Dock property must be set after the Show has bees called (else your child won't be docked).

The bugs in VB.Net 2002

One thing that some people are calling a bug but is not, is that a child form (a from having its MdiParent set to another form) cannot be displayed using the ShowDialog method. It makes sense since a child cannot be modal!

One real bug is about the MaximumSize and MinimumSize properties. Even if you set these properties to some valid values, they won't be considered when the form is displayed as a child. This bug has a well-documented workaround. See http://support.microsoft.com/default.aspx?scid=kb;en-us;327824 . This bug has been fixed in VB.Net 2003.

Another bug is about the Activated event. The event seems to be raised only when no child forms are already displayed. Isn't supposed to be raised every time a form is activated? So don't rely on this event to refresh the state of a toolbar or a menu! This bug has also been fixed in VB.Net 2003. You can test the behaviour by adding some code to the Load and Activated events.

Private Sub ActivatedEvent(ByVal sender As Object, _
                           ByVal e As System.EventArgs) _
                           Handles MyBase.Activated
    Debug.WriteLine("frmChild1 -> Activated")
End Sub

Private Sub LoadEvent(ByVal sender As Object, _
                      ByVal e As System.EventArgs) _
                      Handles MyBase.Load
    Debug.WriteLine("frmChild1 -> Load")
End Sub
The child forms do not get the Form.Activated event (only the parent MDI). To catch MDI children being activated, listen to the Enter/Leave events of that child Form or listen to the Form.MdiChildActivate event in the parent Form.

Another one! The Opacity property has no effects on a child form. I read that Windows (up to XP) can only apply this setting to TopLevel forms (and children forms are not TopLevel). Maybe a next version of Windows will fix it! If someone as Windows 2003 installed, can you please try it for me and drop me a line?

Enough for the bugs (even if I am sure I can find more)!

Using the children collection

It is sometime required to loop through all open children of a MDI. VB.Net maintains a collection of active child forms automatically. This collection is accessible through MDIChildren.

In the next section, we will implement a Close All option that will close all the children. But for now we will implement something that will limit to only one instance of a specific form. I don't know if you tested it before, but if you double click twice on a child node, this child will get two distinct instances. And that's OK for most MDI application. But there are times when you want only one instance of a particular child in memory. This is what this code is doing.

First, here is the code to loop through the collection.

Private Function IsChildInMemory(ByVal pChildName As String) As Boolean
    Dim frmChild As Form
    For Each frmChild In Me.MdiChildren
        If TypeName(frmChild).ToUpper = pChildName.ToUpper Then
            frmChild.Activate()
            Return True
        End If
    Next
    Return False
End Function
As you can see, the MDIChildren collection is a property of the parent form (that's why this code is placed into the parent and uses the Me keyword to access it. This method will only find if the child is already displayed but will also bring it back to front (using the Activate method).

With this code, we can now modify the code that displays the child to limit to one instance a particular form. In the DoubleClick event of the treeview, replace the code to display the second child form to this:

Case "frmChild2"
    If Not IsChildInMemory("frmChild2") Then
        Dim frm As New frmChild2()
        With frm
            .MdiParent = Me
            .Show()
        End With
    End If

Showing the Window menu items

You can easily implement most of the features that you normally find under the Window menu with minimum code. This is what will build in this section.

You first need to start by creating a menu like the one you see in the image.

Figure 1: The Window Menu

The first four elements are really easy to code. A single line is required to implement each of these features. These four options all use "Me.LayoutMdi(MdiLayout.???)" method using a different parameter. The four available values for this parameter are Cascade, TileHorizontal, TileVertical, and ArrangeIcons. Can you guess which value is going with each option? I'll give you a hint: there are in the correct order!

The two remaining elements requires a bit more code, four lines each! In both case, you need to loop through the children collection and take appropriate action. Here is the code that will minimize all children forms:

Dim frmChild As Form
For Each frmChild In Me.MdiChildren
    frmChild.WindowState = FormWindowState.Minimized
Next
For the Close All option, you only need to change the line in the loop for frmChild.Close() and your done.

This menu also normally displays all the open children forms at the bottom with a check to the left of the current active form. This time, you only need to set a property to get this feature. Select your top-level menu item (Window in my example), go to its properties and set the MdiList property to true.

Isn't it easy!

Loading child form from a different assembly

Let's now talk about a really cool feature. What about adding child forms to a parent application that doesn't know anything about it at compile time, except that this application will eventually receive children? I would have needed this feature many times in the past but the solution was not that easy. For example, you can add new features to a parent application without recompiling it. Reflection helps us doing that.

I will not list all the code here. The downloadable demo contains it along with all the code so far. Here is the recipe: 

  • Create and compile an assembly that contains your child forms. This can be a Class Library type project. This is a project called ExternalChildForm in my demo.
  • Create a file containing the list of available external forms (XML is a good candidate for this purpose). This file normally contains information such as the name to display, the location (path and name of the assembly) and the form name. Mine is called DemoCode.exe.config, 
  • Build a container application that will be a MDIContainer. In my case this is the project I use since the beginning. 
  • Make the container application able to load the configuration file. I have created the FillTreeViewFromXML method. 
  • Make the container application able to display external forms. The "Case Else" in my tvwMenu_DoubleClick method handles this.

That's it!

There is an article on MSDN on this subject titled Loading Classes On the Fly that you can read if you're interested in learning more.

Conclusion

I cannot see how creating MDI applications can be easier! The feature of adding dynamic forms to another application so easily is fascinating me.

If you decide to go that way using MDI, there is another topic you will want to explore is the menu merging (displaying specific child form menu elements inside the parent menu.

So I hope you learn new tricks and you appreciate this month column. See you next month.


(Print this page)