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:
- A class that represents documents, derived from
DockContent
- 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()
{
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:
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.