在 Weifen Luo 的 DockPanelSuite 中将内容与容器分离






4.84/5 (10投票s)
使用通用 DockContent 容器类声明式实例化内容的示例。
引言
Weifen Luo 的 Dock Panel Suite 是一个优秀且免费的 Winform 停靠应用程序。然而,对于我的目的来说,它并不完全足够,因为我需要能够动态生成窗体(无论是面板还是文档)的内容,通常是从 XML 生成,而不是在类中硬编码窗体的表示。DockPanelSuite 中的停靠容器是 DockContent
类(我认为它是容器),由于它派生自 System.Windows.Form
,因此在哪里定义了容器的内容。这使得容器(DockContent
)与内容(控件)紧密耦合,虽然在使用窗体设计器时具有明显的吸引力,但它使得动态生成内容或由其他内容实例化系统确定的内容变得困难。本文演示了如何通过重写 DockPanelSuite 的 PersistString
方法来解耦容器与内容。
序列化的停靠状态
Dock Panel Suite 将停靠系统的当前状态序列化为 XML,在“Contents”部分。
<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>
观察“PersistString”属性实际上是如何表示文档或面板内容类的类型名称的。例如,我们注意到 Dock Panel Suite 提供的示例代码具有具体的类,例如,用于文档
public partial class DummyDoc : DockContent { public DummyDoc() { InitializeComponent(); } ... etc ...
以及用于面板的示例
public partial class DummySolutionExplorer : ToolWindow { public DummySolutionExplorer() { InitializeComponent(); } protected override void OnRightToLeftLayoutChanged(EventArgs e) { treeView1.RightToLeftLayout = RightToLeftLayout; } }
解耦具体实现
目标是在文档或面板与实际表示层实现之间添加一个层。因此,我们需要两个通用类:
- 一个代表文档的类,派生自
DockContent
。 - 一个代表面板的类,派生自
ToolWindow
。
而表示层本身将通过单独的实例化引擎进行实例化,而不是由通用类进行实例化。这可以通过一种直接的方式来实现,利用了 PersistString 可以被 DockContent
的子类重写,并在加载布局时传递给实例化器这一事实。GenericDocument
和 GenericPane
类都通过提供自定义持久化字符串来实现这一点,该字符串同时包含类型信息和决定容器内容的元数据。然而,这两个派生类都不负责实例化内容,而是由其他地方完成。
GenericDocument 类
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; } }
GenericPane 类
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; } }
最小化持久化示例
我们现在可以创建一个最小化的应用程序,它:
- 允许我们创建面板和文档。
- 在退出时持久化布局。
- 在应用程序启动时重新加载布局。
创建 DockPanel
需要一个 DockPanel
实例,并且它通常会填充整个客户端区域。
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(); }
这里一个非常重要的点是,DockPanel
实例必须在调用 InitializeComponent
之前被实例化并添加到窗体的控件集合中。
保存布局
这是一个直接调用 Dock Panel Suite API 的操作。
protected void SaveLayout() { dockPanel.SaveAsXml("layout.xml"); }
加载布局
如果布局文件存在,它将被加载。请注意,我们指定了持久化字符串的处理程序来决定如何实例化内容。正如文章开头提到的,我们可以在这里添加自定义处理来解析持久化字符串以获取其他元数据,以决定具体的内容。相反,这里的代码与 Dock Panel Suite 的演示示例非常相似,我们只需要关注我们两个通用内容类型。
protected void LoadLayout() { if (File.Exists("layout.xml")) { dockPanel.LoadFromXml("layout.xml", new DeserializeDockContent(GetContentFromPersistString)); } }
实例化文档和面板
大部分工作从工厂方法 GetContentFromPersistString
开始。请注意元数据是如何从持久化字符串中提取的。
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; }
上述方法实例化了容器(IDockContent
)以及容器的内容。容器本身在此处被实例化:
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; }
此时读者应该注意到改进之处——当内容由具体类(派生自 DockContent
)确定时,我们通常需要修改上述方法来实例化每种新类型的内容。相反,我们拥有通用窗口,并将实际实例化留给另一个组件。
实例化内容
我选择使用 MycroXaml 作为实例化引擎:
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(); }
上述代码将容器(我们的 IDockContent
对象)添加为解析器可以引用的根对象。然后,解析器负责在该容器内实例化内容。
用户界面:创建文档和面板
在演示中,我展示了如何创建一个包含颜色选择器的文档,以及两个面板,一个属性网格和一个解决方案浏览器。根据您停靠窗口的方式,您可能会看到类似这样的内容:
文档和面板的创建与菜单关联,包含内容定义的 XML 文件名在“Tag”属性中设置。
private void OnNewDocument(object sender, EventArgs e) { NewDocument(((ToolStripMenuItem)sender).Tag.ToString()); } private void OnNewPane(object sender, EventArgs e) { NewPane(((ToolStripMenuItem)sender).Tag.ToString()); }
以及 NewDocument
和 NewPane
的实现,仅仅为了显示一些可见的内容:
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); }
XML 内容
本节展示了用于生成不同窗口内容的 XML。
颜色选择器
颜色选择器的定义是:
<?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>
此代码使用几个辅助方法,其中一个是滚动条移动时的事件处理程序:
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); }
请注意,我们如何获取所有三个滚动条的值。这并不是最优雅的方法!
另外,我们需要一个辅助方法来处理数据绑定——作为一个通用评论,我发现经过多年的声明式编程,我更喜欢数据绑定与控件分开处理(而不是作为子元素)。
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); } }
属性网格
属性网格面板的声明非常简单。
<?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>
解决方案浏览器
以及带有硬编码数据的模拟解决方案浏览器的声明也一样。
<?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>
结论
结果布局文件,由 DockPanelSuite 持久化,说明了指定停靠容器实际内容的元数据是如何添加到文档或面板类型中的,这两个现在都是通用容器。
<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>
通过利用 DockPanelSuite 开发者的远见,我展示了如何将内容实例化与容器实例化解耦,这是创建支持动态添加新内容和行为的应用程序的必要步骤。
请注意,此项目的组织结构并不是最优的——它实际上仍处于原型阶段。显然,您可能希望使用其他方法来实例化内容,并且内容不限于 .NET 控件——也可以实例化第三方控件、WPF 等。关键在于,将内容实例化与容器实例化解耦,让我们能够更灵活地使用 DockPanelSuite。