Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Magic Docking - VS.NET Style

0.00/5 (No votes)
29 Sep 2002 34  
Docking Windows mimicking VS.NET feature

Sample Image - MagicDocking.jpg

What are Docking Windows?

One of the first things you notice when using Visual Studio .NET is the clever docking windows implementation. This allows the user to reposition various tool windows such as the Solution Explorer and Properties to dock against different application edges. You can even make them float or become tabbed within the same docking window.

The Magic Library provides an implementation that mimics this clever behaviour and allows you to quickly and easily add the same feature to your own applications.

Downloads

The first download Docking Sample contains a example application that uses the docking windows implementation from the Magic Library. This allows you to experiment and test out the feature. The actual source code is inside the separate second download. At nearly 1MB in size, I thought people would prefer to download the sample before deciding if they want to install the entire source code project!

How do I add docking windows to my own application?

First you need to create an instance of the DockingManager class to be and associate it with each ContainerControl derived object you want to have a docking capability. Most of the time this will be your applications top-level application Form.

As well as a ContainerControl reference the constructor takes a parameter indicating the visual style required. Currently two display styles are supported, VisualStyle.IDE for the Visual Studio .NET appearance and VisualStyle.Plain for the older Visual C++ Version 6 appearance.

The following code shows how to add docking support to a Form

   
using Crownwood.Magic.Common;
using Crownwood.Magic.Docking;

public class MyForm : Form
{
   protected Crownwood.Magic.Docking.
           DockingManager _dockingManager = null;

   public MyForm()
   {
      InitializeComponent();

      // Create the object that manages the 

      // docking state

      _dockingManager = new DockingManager(
                          this, VisualStyle.IDE);
   }
}
Now we need to provide the docking manager with descriptions of each dockable unit. This is the purpose of the Content class. Each Content object creates an association between a title, image and Control derived object. You should read the full documentation of this important class which can be found in the full download.

The following code shows how to create a Content instance and add it to the docking manager inside the form constructor. It creates a RichTextBox control that will act as a dockable notepad for use by the user.

public MyForm()
{
   InitializeComponent();

   _dockingManager = new DockingManager(this,
                                VisualStyle.IDE);

   Content notePad =new Content(_dockingManager);
   
   notePad.Title = "Notepad";
   notePad.Control = new RichTextBox();
   notePad.ImageList = _internalImages;
   notePad.ImageIndex = _imageIndex;

   _dockingManager.Contents.Add(notePad);
}
As this is such a common operation the process has been streamlined. There are several overrides of the Contents.Add method that will create the required Content instance for you during the Add process. Here is the recommended approach.

public MyForm()
{
   InitializeComponent();

   _dockingManager = new DockingManager(this,
                                VisualStyle.IDE);

   // This will create and add a new Content 

   // object all in one operation

   _dockingManager.Contents.Add(
           new RichTextBox(), "Notepad",
                   _internalImages, _imageIndex);
}

Showing and Hiding Contents

Just adding a Content instance will not make it visible to the user. We want our application to make this instance visible immediately, so we use the ShowContent method as shown below: -
public MyForm()
{
   InitializeComponent();

   _dockingManager = new DockingManager(this,
                                VisualStyle.IDE);

   _dockingManager.Contents.Add(new RichTextBox(),
                                   "Notepad", 
                                   _internalImages,
                                   _imageIndex);

   // Make the content with title 'Notepad'

   // visible

   _dockingManager.ShowContent(
            _dockingManager.Contents["Notepad"]);
}
This shows how to find a reference to a Content object by using the string indexer of the Contents collection. In this particular case it is a little inefficient as we could have stored the Content reference that is returned from the call to Contents.Add. Here is a more efficient example: -
public MyForm()
{
   InitializeComponent();

   _dockingManager = new DockingManager(this,
                                VisualStyle.IDE);

   Content notePad = _dockingManager.
           Contents.Add(new RichTextBox(),
                 "Notepad", _internalImages,
                                    _imageIndex);

   _dockingManager.ShowContent(notePad);
}
At some point in the future you may want to hide this instance again in which case you can use the HideContent method. To make all the Content instances visible or invisible use the ShowAllContent and HideAllContent methods respectively.

Accurate Creation

Three lines of code and we have a docking window made visible to the user which can be redocked and resized. However, at no point so far have we specified exactly where the new Content gets shown. The docking position for a Content made visible is the saved position from when it was last hidden. In our case the instance has never been hidden because it has just been created.

The constructor for the Content will default the saved docking position to be the left edge. Therefore our last example above will display the content inside a docking window which is docked against the left edge of the application Form. The value of the Content.DisplaySize will be used to decide how wide the docking window should be, this defaults to 150, 150.

If you want to dock against a different edge or even begin in the floating state then you need to do a little more work. The following code shows the use of the AddContentWithState method to show the content with a defined initial state: -

public MyForm()
{
   InitializeComponent();

   _dockingManager = new DockingManager(this,
                                VisualStyle.IDE);

   Content notePad = 
              _dockingManager.Contents.Add(
                  new RichTextBox(), "Notepad",
                   _internalImages, _imageIndex);

   // Request new Docking window be created and

   // displayed on the right edge

   _dockingManager.AddContentWithState(notePad,
                                State.DockRight);
}

Create in same window

Using the above method allows a docking window to be made visible and its position defined. But it does have the drawback that it will always create a new docking window to host the Content instance. What if we want two or more Content objects to be hosted inside the same docking window? To achieve this we need to bring another method called AddContentToWindowContent into use.

Each content is always hosted inside a WindowContent derived object. We can remember the reference of the newly created WindowContent object and reuse it as the destination for other Content instances. The following example creates notePad instances that are placed inside the same docking window, when this happens the docking window will adopt a tabbed appearance: -

public MyForm()
{
   InitializeComponent();

   _dockingManager = new DockingManager(this,
                                VisualStyle.IDE);

   Content notePad1 =
           _dockingManager.Contents.Add(
              new RichTextBox(), "Notepad1",
                   _internalImages, _imageIndex);

   Content notePad2 = 
           _dockingManager.Contents.Add(
              new RichTextBox(), "Notepad2",
                   _internalImages, _imageIndex);

   WindowContent wc =
           _dockingManager.AddContentWithState(
                     notePad1, State.DockRight) 
                                as WindowContent;

   // Add the second notePad2 to the same

   // WindowContent

   _dockingManager.AddContentToWindowContent(
                                  notePad2, wc);
}

Create in same Column/Row

There is only one more ability we need to add so that any docking configuration can be constructed at start-up. We need the ability to place docking windows in the same column or row. To do this we have to understand more about the actual structure of objects maintained by the docking code.

Docking is supported by providing three levels of object. Each Content object exists inside a Window derived object which itself exists inside a Zone derived object. The WindowContent class is a specialization of the Window base class that has special knowledge about how to handle Content objects. It is easiest to explain by providing some examples.

The AddContentWithState method creates a new WindowContent instance and adds to it the provided Content parameter. Next a Zone is created and the WindowContent instance placed inside it. The Zone is then added to the hosting Form and positioned according to the State parameter.

The AddContentToWindowContent adds the provided Content parameter to the existing WindowContent instance.

The AddContentToZone method creates a new WindowContent instance and adds to it the provided Content parameter. It then adds the new WindowContent to the provided Zone in the correct relative position.

The following example shows how to create three notePad objects, where the first two are added to the same WindowContent causing a tabbed appearance to occur. The final notePad is created in its own WindowContent and then added to the same Zone. The position value of 0 will make the second WindowContent be positioned first in the Zone.

public MyForm()
{
   InitializeComponent();

   _dockingManager = new DockingManager(this,
                                VisualStyle.IDE);

   Content notePad1 = 
          _dockingManager.Contents.Add(
               new RichTextBox(), "Notepad1",
                   _internalImages, _imageIndex);

   Content notePad2 = 
          _dockingManager.Contents.Add(
               new RichTextBox(), "Notepad2",
                   _internalImages, _imageIndex);

   Content notePad3 = 
          _dockingManager.Contents.Add(
               new RichTextBox(), "Notepad3", 
                  _internalImages, _imageIndex);

   WindowContent wc = 
         _dockingManager.AddContentWithState(
                     notePad1, State.DockRight) 
                                as WindowContent;

   _dockingManager.AddContentToWindowContent(
                                   notePad2, wc);

   // Add a new WindowContent to the existing

   // Zone already created

   _dockingManager.AddContentToZone(notePad3,
                               wc.ParentZone, 0);
}
You can use the Content.ParentWindowContent property to discover which WindowContent a Content instance is currently placed inside. Likewise, the WindowContent.ParentZone property indicates the Zone a WindowContent instance is inside. Using these and the above-described methods should allow any start up configuration to be constructed. Note that the Content.ParentWindowContent property returns null if the Content is not currently visible.

Control where docking can occur

If you use the SampleDocking application you will notice that you cannot redock a docking window between a Form edge and either the MenuControl or StatusBar controls. In order to achieve this effect we need to define a couple of the docking manager properties.

The OuterControl property needs to be set to the first control in the Forms.Control collection that represents the group of controls that the manager must not dock between. Remember that the order of controls in the Form.Control collection determines the sizing and positioning of them. So the last control in the collection is the one that is positioned and sized first, the second to last control will be positioned and sized in the space that remains.

As the MenuControl is the most important and needs to be positioned first it will be last in the collection. The StatusBar is the next most important and so is second to last in the collection. In this scenario the OuterControl would be set to a reference of the StatusBar control. This will prevent the docking manager from reordering any window after the StatusBar in the collection. If the StatusBar was last in the list and the MenuControl second to last then the OuterControl would need to reference the MenuControl instead.

The InnerControl property needs to be set to the last control in the Forms.Control collection that represents the group of controls that the manager must not dock after. This might seem odd, as there is unlikely to be a docked window you would not want the docking windows to be placed inside of. However there is a situation where this becomes important.

If you have a control that is defined as having the Dock property of DockStyle.Fill then this control must always occur in the Form.Control collection before any docking windows. Otherwise you can get the situation where the control with the DockStyle.Fill value is not sized according to the actual space left over when all docking windows have been placed. Because this control is further up the list of controls it calculates its size without taking into account any docking windows that occur earlier in the collection.

The following shows a MenuControl, StatusBar and a RichTextBox being created and added to the Form.Control collection. It then sets the correct InnerControl and OuterControl values to generate the expected runtime operation.

public MyFormConstructor()
{
   // This block would normally occur inside

   // a call to:-

   // InitializeComponent();

   {
      RichTextBox filler = new RichTextBox();
      filler.Dock = DockStyle.Fill;
      Controls.Add(filler);

      StatusBar status = new StatusBar();
      status.Dock = DockStyle.Bottom;
      Controls.Add(status);

      MenuControl menu = new MenuControl();
      menu.Dock = DockStyle.Top;
      Controls.Add(menu);
   }

   _dockingManager = new DockingManager(this,
                                VisualStyle.IDE);

   _dockingManager.InnerControl = filler;
   _dockingManager.OuterControl = status;

   // Now create and setup my Content objects

   ...
}

Persistence

Many applications need to be able to remember several different docking configurations of the Content objects and be able to switch between them at runtime. You might also want to save the configuration when the application is shutdown so that it can be restored at start-up. The code to save and load configurations is as follows:-

// Save the current configuration to a named file

 _dockingManager.SaveConfigToFile(
                           "MyFileName.xml");

 ...

// Load a saved configuration and apply

// immediately

_dockingManager.LoadConfigFromFile(
                         "MyFileName.xml");
There are a couple of issues to remember though. The saving process does not save the actual Content objects but just the state information it needs in order to restore that Content to the same docking size/position later. So the Content object must already exist and be part of the docking manager when the load operation takes place because loading will not recreate those objects.

The second point is that the save and load use the Title property of the Content to identify the information. If you change the Title of a Content object between saving and loading then the latter process will fail to associate the saved information to the object. This will not cause an exception but that Content will not be updated with the required configuration.

If you need to save the configuration information into a database or simply save it internally then you do not have to save into a file. There are matching methods for saving and loading into byte arrays which are easy to store within your application or to a database. For even greater control use the methods that take a stream object instance, in which case you must create and provide the stream object instance, but this gives the developer complete control over the storage medium.

Some developers might find it useful to save and load some additional custom details inside the configuration data. This prevents the need for two sets of saved data which then need to be maintained in parallel. The SaveCustomConfig event is generated when all the docking information has been written and allows you to add additional information at the end.

On loading the LoadCustomConfig is event is generated so that the corresponding information can be read back in again. The following sample code shows a trivial example of this:-

public void InitialiseComponent()   
{
   ...
      
   // Setup custom config handling at appropriate

   // place in code

   _manager.SaveCustomConfig += 
      new DockingManager.SaveCustomConfigHandler
                                  (OnSaveConfig);
         
   _manager.LoadCustomConfig += 
      new DockingManager.LoadCustomConfigHandler
                                  (OnLoadConfig);
}
   
protected void OnSaveConfig(XmlTextWriter xmlOut)
{ 
   // Add an extra node into the config to store

   // some example information

   xmlOut.WriteStartElement("MyCustomElement");
   xmlOut.WriteAttributeString("ExampleData1",
                                        "Hello");
   xmlOut.WriteAttributeString("ExampleData2",
                                       "World!");
   xmlOut.WriteEndElement();
}

protected void OnLoadConfig(XmlTextReader xmlIn)
{
   // We are expecting our custom element to be

   // the current one

   if (xmlIn.Name == "MyCustomElement")
   {
      // Read in both the expected attributes

      string attr1 = xmlIn.GetAttribute(0);
      string attr2 = xmlIn.GetAttribute(1);

      // Must move past our element

      xmlIn.Read();
   }
}

Known problems

One of nice features of the docking windows is the ability to take a Form derived class and use it as the content of a docking window. You need to be careful though, as sometimes a Form you generate will have the AutoScaleBaseSize property define. This can cause the Form instance to be sized incorrectly when it is shown for the first time. If someone comes up with an answer to this problem then please let me know!

A second problem is the use of the RichTextBox. If you use this as the content for the docking window and then move the docking window to a different edge it will sometimes cause the control to be recreated. In this case it loses any coloring information. So your blue/red text inside the RichTextBox suddenly becomes the default black. Again, if anyway knows how to fix this issue then please let me know.

Revision History

20 Sept 2002 - Initial revision

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here