Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / XAML

Decoupling Content From Container in Weifen Luo's DockPanelSuite

4.84/5 (10 votes)
10 Jan 2013CPOL5 min read 44.8K   3.5K  
An example of declarative instantiation of content with a generic DockContent container class.

Introduction

Weifen Luo's Dock Panel Suite is an excellent and free winform docking application.  However, for my purposes it is not entirely adequate because I require the ability to dynamically generate the content of the forms (whether panes or documents) at runtime, usually from XML, rather than the form's presentation being harcoded in a class.  The docking container in DockPanelSuite is the DockContent class (which I consider to be the container), and, because it is derived from System.Windows.Form, is where the content of the container is defined.  This intimately couples the container (DockContent) with the content (the controls) and while this has a definite appeal when using a form designer, it makes it difficult to generate dynamic content or content that is determined by other systems of content instantiation.  This article shows how to decouple the container from the content, using the

PersistString 
overridable methods of the DockPanelSuite.

The Serialized Dock State

The Dock Panel Suite serializes the current state of the docking system to XML in a "Contents" section:

<Contents Count="7">
  <Content ID="0" PersistString="DockSample.DummyDoc,,Document1" AutoHidePortion="0.25" IsHidden="False" IsFloat="False"/>
  <Content ID="1" PersistString="DockSample.DummySolutionExplorer" AutoHidePortion="0.25" IsHidden="False" IsFloat="False"/>
  <Content ID="2" PersistString="DockSample.DummyPropertyWindow" AutoHidePortion="0.25" IsHidden="False" IsFloat="False"/>
  <Content ID="3" PersistString="DockSample.DummyToolbox" AutoHidePortion="0.25" IsHidden="False" IsFloat="False"/>
  <Content ID="4" PersistString="DockSample.DummyOutputWindow" AutoHidePortion="0.25" IsHidden="False" IsFloat="False"/>
  <Content ID="5" PersistString="DockSample.DummyTaskList" AutoHidePortion="0.25" IsHidden="False" IsFloat="False"/>
  <Content ID="6" PersistString="DockSample.DummyDoc,,Document2" AutoHidePortion="0.25" IsHidden="False" IsFloat="False"/>
</Contents>

Observe how the "PersistString" attribute is essentially the type name of the class that represents the content of the document or pane.  So, for example, we note that the sample code provided with the Dock Panel Suite has concrete classes, such as, for a document:

public partial class DummyDoc : DockContent
{
  public DummyDoc()
  {
    InitializeComponent();
  }
... etc ...

and for an example of a pane:

public partial class DummySolutionExplorer : ToolWindow
{
  public DummySolutionExplorer()
  {
    InitializeComponent();
  }

  protected override void OnRightToLeftLayoutChanged(EventArgs e)
  {
    treeView1.RightToLeftLayout = RightToLeftLayout;
  }
}

Decoupling The Concrete Implementation

The goal is to add a layer of between the document or pane and the actual implementation of the presentation.  Thus, we need two general classes:

  1. A class that represents documents, derived from DockContent
  2. A class that represents panes, derived from ToolWindow

And the presentation itself will be instantiated, not by the general class, but by a separate instantiation engine.  This is accomplished in a straight forward manner, taking advantage of the fact that the PersistString is overridable by a DockContent child class and is passed in to the instantiator when the layout is loaded.  Both the GenericDocument and

GenericPane 
classes take advantage of this by providing a custom persistence string which provides both the type information and the metadata that determines the container's content.  Neither of these derived classes however are responsible for instantiating the content - this is done elsewhere.

The GenericDocument Class

public class GenericDocument : DockContent, IGenericDock
{
  public string ContentMetadata { get; set; }

  public GenericDocument()
  {
    ContentMetadata = String.Empty;
  }

  public GenericDocument(string contentMetadata)
  {
    ContentMetadata = contentMetadata;
  }

  protected override string GetPersistString()
  {
    return GetType().ToString() + "," + ContentMetadata;
  }
}

The GenericPane Class

public class GenericPane : ToolWindow, IGenericDock
{
  public string ContentMetadata { get; set; }

  public GenericPane()
  {
    ContentMetadata = String.Empty;
  }

  public GenericPane(string contentMetadata)
  {
    ContentMetadata = contentMetadata;
  }

  protected override string GetPersistString()
  {
    return GetType().ToString() + "," + ContentMetadata;
  }
}

A Minimal Persistence Example

We can now create a minimalist application that:

  • allows us to create panes and documents
  • persists the layout on exit
  • reloads the layout when the application is started

Creating the DockPanel

A DockPanel instance is required and usually fills the entire client area:

public Form1()
{
  // Must be set to true for MDI docking style.
  IsMdiContainer = true;

  dockPanel = new DockPanel();
  dockPanel.Dock = DockStyle.Fill;
  Controls.Add(dockPanel);

  InitializeComponent();
  LoadLayout();
}

A very important point here is that the DockPanel instance must be instantiated and added to the form's control collection before the call to

InitializeComponent
.

Saving the Layout

This is a straight forward call to the Dock Panel Suite API:

protected void SaveLayout()
{
  dockPanel.SaveAsXml("layout.xml");
}

Loading the Layout

If the layout file exists, it is loaded.  Note that we specify the handler for the persist string to determine how to instantiate the content.  As mentioned in the beginning of the article, here is where we could have added custom handling to parse the persist string for additional metadata to determine the specific content.  Instead, the code here is very similar to the Dock Panel Suite demo example, and we need to worry only about our two general content types:

protected void LoadLayout()
{
  if (File.Exists("layout.xml"))
  {
    dockPanel.LoadFromXml("layout.xml", new DeserializeDockContent(GetContentFromPersistString));
  }
}

Instantiating Documents and Panes

The bulk of the work is done starting in the factory method GetContentFromPersistString.  Observe how the metadata is extracted from the persist string.

protected IDockContent GetContentFromPersistString(string persistString)
{
  string typeName = persistString.LeftOf(',').Trim();
  string contentMetadata = persistString.RightOf(',').Trim();
  IDockContent container = InstantiateContainer(typeName, contentMetadata);
  InstantiateContent(container, contentMetadata);

  return container;
}

The above method instantiated the container (IDockContent) as well as the content for the container.  The container itself is instantiated here:

protected IDockContent InstantiateContainer(string typeName, string metadata)
{
  IDockContent container = null;

  if (typeName == typeof(GenericPane).ToString())
  {
    container = new GenericPane(metadata);
  }
  else if (typeName == typeof(GenericDocument).ToString())
  {
    container = new GenericDocument(metadata);
  }

  return container;
}

The reader should at this point note the improvement - when the content is determined by a concrete class (derived from DockContent), we would normally need to modify the above method for every new type of content in order to instantiate it.  Instead, we have general purpose windows and we leave the actual instantiation to another component.

Instantiating the Content

I decided to use MycroXaml as the instantiation engine: 

public void InstantiateContent(object container, string filename)
{
  MycroParser mp = new MycroParser();
  mp.AddInstance("Container", container);

  XmlDocument doc = new XmlDocument();
  doc.Load(filename);
  mp.Load(doc, "Form", this);
  mp.Process();
}

The above code adds the container (our IDockContent object) as an object the parser can reference as the root object.  The parser is then responsible for instantiating the content within that container.

User Interface: Creating Documents and Panes

In the demo, I illustrate creating a document which consists of a color chooser, and two panes, a property grid and a solution explorer.  Depending on how you dock your windows, you can get something that looks like this:

Image 1

Documents and pane creation is wired to the menu and the XML filename containing the content definition is set in the "Tag" property:

private void OnNewDocument(object sender, EventArgs e)
{
  NewDocument(((ToolStripMenuItem)sender).Tag.ToString());
}

private void OnNewPane(object sender, EventArgs e)
{
  NewPane(((ToolStripMenuItem)sender).Tag.ToString());
}

and the implementation for NewDocument and NewPane, just to get something visible showing:

protected void NewDocument(string filename)
{
  GenericDocument doc = new GenericDocument(filename);
  InstantiateContent(doc, filename);
  doc.Show(dockPanel);
}

protected void NewPane(string filename)
{
  GenericPane pane = new GenericPane(filename);
  InstantiateContent(pane, filename);
  pane.Show(dockPanel);
}

The XML Content

This section shows the XML used to generate the content for the different windows.

The Color Picker

The definition for the color picker is:

<?xml version="1.0" encoding="utf-8"?>
<MycroXaml Name="Form"
  xmlns:wf="System.Windows.Forms, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
  xmlns:mc="Clifton.Tools.Xml, GenericDockPanelSuite"
  xmlns:dps="GenericDockPanelSuite, GenericDockPanelSuite"
  xmlns:ref="ref">
  <dps:GenericPane ref:Name="Container"
    Text="Color Chooser"
    ClientSize="400, 190"
    BackColor="White">
    <dps:Controls>
      <wf:TrackBar Name="RedScroll" Orientation="Vertical" TickFrequency="16" TickStyle="BottomRight" Minimum="0" Maximum="255" Value="128" Scroll="OnScrolled" Size="42, 128" Location="10, 30"/>
      <wf:TrackBar Name="GreenScroll" Orientation="Vertical" TickFrequency="16" TickStyle="BottomRight" Minimum="0" Maximum="255" Value="128" Scroll="OnScrolled" Size="42, 128" Location="55, 30"/>
      <wf:TrackBar Name="BlueScroll" Orientation="Vertical" TickFrequency="16" TickStyle="BottomRight" Minimum="0" Maximum="255" Value="128" Scroll="OnScrolled" Size="42, 128" Location="100, 30"/>

      <wf:Label Size="40,15" TextAlign="TopCenter" Font="Microsoft Sans Serif, 8.25pt, style= Bold" Location="10, 10" ForeColor="Red" Text="Red"/>
      <wf:Label Size="40,15" TextAlign="TopCenter" Font="Microsoft Sans Serif, 8.25pt, style= Bold" Location="55, 10" ForeColor="Green" Text="Green"/>
      <wf:Label Size="40,15" TextAlign="TopCenter" Font="Microsoft Sans Serif, 8.25pt, style= Bold" Location="100, 10" ForeColor="Blue" Text="Blue"/>

      <wf:Label Name="RedValue" Size="40,15" TextAlign="TopCenter" Font="Microsoft Sans Serif, 8.25pt, style= Bold" Location="10, 160" ForeColor="Red"/>
      <wf:Label Name="GreenValue" Size="40,15" TextAlign="TopCenter" Font="Microsoft Sans Serif, 8.25pt, style= Bold" Location="55, 160" ForeColor="Green"/>
      <wf:Label Name="BlueValue" Size="40,15" TextAlign="TopCenter" Font="Microsoft Sans Serif, 8.25pt, style= Bold" Location="100, 160" ForeColor="Blue"/>

      <wf:PictureBox Name="ColorPanel" Location="90, 0" Size="200, 100" Dock="Right" BorderStyle="Fixed3D" BackColor="128, 128, 128"/>
    </dps:Controls>

    <mc:DataBinding Control="{RedValue}" PropertyName="Text" DataSource="{RedScroll}" DataMember="Value"/>
    <mc:DataBinding Control="{GreenValue}" PropertyName="Text" DataSource="{GreenScroll}" DataMember="Value"/>
    <mc:DataBinding Control="{BlueValue}" PropertyName="Text" DataSource="{BlueScroll}" DataMember="Value"/>
  </dps:GenericPane>
</MycroXaml>    

This code uses a couple helpers, one of which is the event handler for when the scrollbars are moved:

protected void OnScrolled(object sender, EventArgs e)
{
  TrackBar redScroll = (TrackBar)((Control)sender).FindForm().Controls.Find("RedScroll", false)[0];
  TrackBar greenScroll = (TrackBar)((Control)sender).FindForm().Controls.Find("GreenScroll", false)[0];
  TrackBar blueScroll = (TrackBar)((Control)sender).FindForm().Controls.Find("BlueScroll", false)[0];
  PictureBox colorPanel = (PictureBox)((Control)sender).FindForm().Controls.Find("ColorPanel", false)[0];

  colorPanel.BackColor = System.Drawing.Color.FromArgb((byte)redScroll.Value, (byte)greenScroll.Value, (byte)blueScroll.Value);
}

Note how we obtain the values of all three scrollbars.  Not the most elegant approach!

Also, we need a helper to do the data binding - as a general comment, I have discovered after years of declarative programming that I prefer the data binding to be handled separately (rather than as sub-elements) to the controls:

public class DataBinding : ISupportInitialize
{
  public string PropertyName { get; set; }
  public Control Control { get; set; }
  public object DataSource { get; set; }
  public string DataMember { get; set; }

  public void BeginInit()
  {
  }

  public void EndInit()
  {
    Control.DataBindings.Add(PropertyName, DataSource, DataMember);
  }
}

The Property Grid

The declaration for the property grid pane is trivial:

<?xml version="1.0" encoding="utf-8" ?>
<MycroXaml Name="Form"
  xmlns:wf="System.Windows.Forms, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
  xmlns:mc="Clifton.Tools.Xml, GenericDockPanelSuite"
  xmlns:dps="GenericDockPanelSuite, GenericDockPanelSuite"
  xmlns:ref="ref">
  <dps:GenericPane ref:Name="Container"
    TabText="Properties"
    ClientSize="400, 190"
    BackColor="White"
    ShowHint="DockRight">
    <dps:Controls>
      <wf:PropertyGrid Dock="Fill" SelectedObject="{Container}"/>
    </dps:Controls>
  </dps:GenericPane>
</MycroXaml>

The Solution Explorer

as is the one for the mockup solution explorer with some hard-coded data:

<?xml version="1.0" encoding="utf-8" ?>
<MycroXaml Name="Form"
  xmlns:wf="System.Windows.Forms, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
  xmlns:mc="Clifton.Tools.Xml, GenericDockPanelSuite"
  xmlns:dps="GenericDockPanelSuite, GenericDockPanelSuite"
  xmlns:ref="ref">
  <dps:GenericPane ref:Name="Container"
    TabText="Solution Explorer"
    ClientSize="400, 190"
    BackColor="White"
    ShowHint="DockLeft">
    <dps:Controls>
      <wf:TreeView Dock="Fill">
        <wf:Nodes>
          <wf:TreeNode Text="Solution 'Generic Dock Panel Suite">
            <wf:Nodes>
              <wf:TreeNode Text="Demo"/>
              <wf:TreeNode Text="GenericDockPanelSuite"/>
              <wf:TreeNode Text="MycroXamlDemo"/>
              <wf:TreeNode Text="WinFormsUI"/>
            </wf:Nodes>
          </wf:TreeNode>
        </wf:Nodes>
      </wf:TreeView>
    </dps:Controls>
  </dps:GenericPane>
</MycroXaml>  

Conclusion

The resulting layout file, as persisted by DockPanelSuite, illustrates how the metadata that specifies the actual content of a docking container is added to the document or pane type, both of which are now general purpose containers.

<Contents Count="4">
  <Content ID="0" PersistString="GenericDockPanelSuite.GenericDocument,colorPicker.mxaml" AutoHidePortion="0.25" IsHidden="False" IsFloat="False" Tag="" />
  <Content ID="1" PersistString="GenericDockPanelSuite.GenericPane,propertyGrid.mxaml" AutoHidePortion="0.25" IsHidden="False" IsFloat="False" Tag="" />
  <Content ID="2" PersistString="GenericDockPanelSuite.GenericDocument,colorPicker.mxaml" AutoHidePortion="0.25" IsHidden="False" IsFloat="False" Tag="" />
  <Content ID="3" PersistString="GenericDockPanelSuite.GenericPane,solutionExplorer.mxaml" AutoHidePortion="0.25" IsHidden="False" IsFloat="False" Tag="" />
</Contents>

Leveraging the foresight of the developer of DockPanelSuite, I demonstrated how we can decouple the instantiation of the content from that of the container, which is a necessary step towards creating an application that supports new content and behavior on the fly.

Note that the organization of this project isn't the greatest - it is actually still in the prototype stages.  Obviously, you may want to use some other method of instantiating content, and the content isn't restricted to .NET controls - third party controls, WPF, etc., could be instantiated.  The point is, decoupling the content instantiation from the container instantiation gives us greater flexibility with the DockPanelSuite.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)