从内部 WinForms 设计器“继承”






4.91/5 (15投票s)
通过在自定义组件设计器中封装来定制内部设计器
引言
设计器通常是扩展关联组件在设计模式下行为的最佳选择。虽然存在其他方法,如 TypeDescriptionProvider
、ITypeDescriptorFilterService
或重写 Component.Site
属性;但设计器仍然是最简单、最简洁的方法。由于大多数组件都依赖于公共框架 ComponentDesigner
或 ControlDesigner
,因此使用派生的自定义设计器不会有问题。
当设计器被标记为 internal
且属于 System.Design
程序集,并且难以通过“借用”微软的代码来重现时,问题就开始出现了。自定义智能标签是其中一项功能,如果没有自定义设计器,几乎是不可能实现的。有问题的隐藏设计器示例包括 ToolStrip
/ ToolStripItem
组件,它们拥有大量相互依赖的内部类,以增强我们的 IDE 体验。
我提出一个简单的想法,即使用内部的默认设计器,通过将其封装在合适的 ComponentDesigner
或 ControlDesigner
中,并将成员调用委托给内部设计器。本文重点介绍了一些使其正常工作的、不太明显的潜在问题。演示项目使用 ContextMenuStrip
和 TreeView
控件,没有任何实际功能的添加,以此作为概念验证。
自定义设计器骨架
框架中的 ContextMenuStrip
是一个 Control
,但与之关联的 ToolStripDropDownDesigner
派生自 ComponentDesigner
。因此,我们的自定义设计器也将如此。
internal abstract class ToolStripDropDownDesigner : ComponentDesigner
{
protected ComponentDesigner defaultDesigner;
public override void Initialize(IComponent component)
{
// internal class ToolStripDropDownDesigner : ComponentDesigner
// Name: System.Windows.Forms.Design.ToolStripDropDownDesigner ,
// Assembly: System.Design, Version=4.0.0.0
Type tDesigner = Type.GetType
("System.Windows.Forms.Design.ToolStripDropDownDesigner, System.Design");
defaultDesigner = (ComponentDesigner)Activator.CreateInstance
(tDesigner, BindingFlags.Instance | BindingFlags.Public, null, null, null);
defaultDesigner.Initialize(component);
base.Initialize(component);
}
public override void InitializeNewComponent(IDictionary defaultValues)
{
base.InitializeNewComponent(defaultValues);
defaultDesigner.InitializeNewComponent(defaultValues);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (defaultDesigner != null)
{
defaultDesigner.Dispose();
}
}
base.Dispose(disposing);
}
}
设计器属性
设计器可以公开仅用于设计时期的属性,添加新属性或覆盖现有控件属性。这些属性可以标记为 private
,因为设计时环境使用反射来读取和设置值。现在,我们的自定义设计器已通过 DesignerAttribute
指定为主设计器,并且将查询我们的设计器而不是默认设计器来获取属性。这是否意味着我们必须在自定义设计器中重写所有设计器属性,并使用反射来委托所有调用?
幸运的是,插入一行代码将使我们摆脱这种麻烦。
public override void Initialize(IComponent component)
{
...
// use Designer properties of nested designer ( do before base.Initialize ! )
TypeDescriptor.CreateAssociation(component, defaultDesigner);
defaultDesigner.Initialize(component);
base.Initialize(component);
}
引自 MSDN
"
CreateAssociation
方法创建主对象和辅助对象之间的关联。一旦创建了关联,设计器或其他筛选机制就可以将路由到任一对象的属性添加到主对象的属性集中。当对主对象进行属性调用时,将调用GetAssociation
方法来解析与类型参数相关的实际对象实例。"
为了清楚起见:我们设计器上定义的任何属性也会被查询,我们只是创建了一个额外的目标。顺便说一句,CreateAssociation()
是我几年前首次尝试封装失败时缺失的那一块。
IDesignerFilter 方法
ComponentDesigner
继承自 IDesignerFilter
接口,我们必须重写其方法以将调用委托给默认设计器。由于这些方法被标记为 protected
,我们将设计器强制转换为接口,以便访问它们。
protected IDesignerFilter designerFilter;
designerFilter = defaultDesigner;
ComponentDesigner
的 PreFilterAttributes()
和 PreFilterEvents()
实现是空的,而 PostFilterProperties()
只处理组件继承自 IPersistComponentSettings
的罕见情况。我们不会费心重写这些方法。
protected override void PreFilterProperties(IDictionary properties)
{
// base.PreFilterProperties(properties) omitted, as designerFilter calls it as well
designerFilter.PreFilterProperties(properties);
}
继承的组件行为不同,它们继承了大部分属性值自其基类实例。组件继承由设计器上 protected
的 'Inherited
' (bool
) 和 'InheritanceAttribute
' 属性标识。不知何故,只有默认设计器被 InheritanceAttribute
装饰,而我们的设计器从未被装饰,并且始终报告:未继承。当我们知道 ComponentDesigner
的 PostFilterAttributes()
实现会同步属性存在性与 'InheritanceAttribute
' 属性值时,修复就变得很容易了。
protected override void PostFilterAttributes(IDictionary attributes)
{
designerFilter.PostFilterAttributes(attributes);
// will set this.InheritanceAttribute property from
// nested designer attributes, if control is inherited
base.PostFilterAttributes(attributes);
#if DEBUG
if (attributes.Contains(typeof(InheritanceAttribute)))
{
Debug.Assert(base.InheritanceAttribute ==
attributes[typeof(InheritanceAttribute)]);
}
else
{
Debug.Assert(base.InheritanceAttribute == InheritanceAttribute.NotInherited);
}
#endif
}
protected override void PostFilterEvents(IDictionary events)
{
// filters events based on InheritanceAttribute
designerFilter.PostFilterEvents(events);
}
现在,我们设计好的继承控件行为正确,所有属性都显示为只读,这就像所有 ToolStrip
在继承时表现出的那种讨厌的方式一样。
DesignerActionList 和 Verbs
我的最初目标是通过重写 'ActionLists
' 属性来定制智能标签,然而事实证明只有默认设计器上的属性才会被查询。对于 'Verbs
',情况则相反,只有我的设计器被调用。仔细检查后发现,ComponentDesigner.Initialize()
将一个 DesignerCommandSet
实例注册为站点特定的服务。然后,DesignerActionService
(管理智能标签和设计器动词功能)会从此服务查询。
DesignerCommandSet
是 public
的,并且 ComponentDesigner.Initialize()
仅在服务尚不存在时才注册其版本。因此,通过添加我们自己的版本来路由调用到我们设计器的属性,并在初始化两个设计器之前注册它,就可以轻松完成修复。
private class CDDesignerCommandSet : DesignerCommandSet
{
private readonly ComponentDesigner componentDesigner;
public CDDesignerCommandSet(ComponentDesigner componentDesigner)
{
this.componentDesigner = componentDesigner;
}
public override ICollection GetCommands(string name)
{
if (name.Equals("Verbs"))
{
// componentDesigner.Verbs & defaultDesigner.Verbs are empty
return null;
}
if (name.Equals("ActionLists"))
{
return componentDesigner.ActionLists;
}
return base.GetCommands(name);
}
}
public override void Initialize(IComponent component)
{
...
IServiceContainer site = (IServiceContainer)component.Site;
site.AddService(typeof(DesignerCommandSet), new CDDesignerCommandSet(this));
defaultDesigner.Initialize(component);
base.Initialize(component);
}
public override DesignerActionListCollection ActionLists
{
get { return defaultDesigner.ActionLists; }
}
如果您不需要自定义智能标签,则可以删除上述代码。
其他重写
您必须分析默认设计器还重写了哪些其他成员,并在自定义设计器中重新实现它们。对于 ContextMenuStrip
,只有一个属性被证明是必要的,那就是返回包含的 ToolStripItem
集合。
public override ICollection AssociatedComponents
{
get { return defaultDesigner.AssociatedComponents; }
}
除了静态分析,您还必须测试被重写和内部成员是否被正确调用。TreeView
的默认设计器重写了 ControlDesigner.OnPaintAdornments()
,但令我惊讶的是,该方法在两个设计器上都被调用,并且协同工作得很好。
DemoContextStripDesigner
您可能已经注意到,我将自定义的 ToolStripDropDownDesigner
声明为 abstract class
,它可以重用于所有 ToolStripDropDown
组件。演示设计器作为概念验证,仅添加了一个新的设计器属性,并删除了智能标签中的所有标准条目,除了 'Edit Items' 动词。
internal class DemoContextStripDesigner : ToolStripDropDownDesigner
{
private bool myVar;
public bool MyProperty
{
get { return myVar; }
set { myVar = value; }
}
protected override void PreFilterProperties
(System.Collections.IDictionary properties)
{
base.PreFilterProperties(properties);
PropertyDescriptor pd = TypeDescriptor.CreateProperty(
GetType(), "MyProperty", typeof(bool),
new DescriptionAttribute("Designer Property"),
new DesignerSerializationVisibilityAttribute
(DesignerSerializationVisibility.Hidden));
properties.Add(pd.Name, pd);
}
public override DesignerActionListCollection ActionLists
{
get
{
DesignerActionListCollection actionLists = base.ActionLists;
actionLists.RemoveAt(0);
return actionLists;
}
}
}
ControlDesigner 的特殊性
ControlDesigner.Initialize()
向 DesignerActionService
添加了一个 private DockingActionList
以支持可停靠控件,导致我们显示了两次“Dock/Undock in Parent Container”动词。我没有找到其他方法,只能使用反射的“黑魔法”在初始化两个设计器后移除一个列表。
private void removeDuplicateDockingActionList()
{
// ControlDesigner field : private DockingActionList dockingAction;
FieldInfo fi = typeof(ControlDesigner).GetField("dockingAction",
BindingFlags.Instance | BindingFlags.NonPublic);
if (fi != null)
{
DesignerActionList dockingAction = (DesignerActionList)fi.GetValue(this);
if (dockingAction != null)
{
DesignerActionService service = (DesignerActionService)
GetService(typeof(DesignerActionService));
if (service != null)
{
service.Remove(Control, dockingAction);
}
}
}
}
关注点
遗憾的是,System.Design
程序集并未包含在 Microsoft Reference Source 中。如果您认真从事设计时或程序包开发,请获取 .NET Reflector Pro 插件。在临时项目引用中,System.Design.dll 和其他 Microsoft.VisualStudio.* 程序集,确保它们被加载,并利用试用期反编译它们,以便在调试时进行源码调试。为 .NET 2.0 和 .NET 4.0 框架各进行一次。请注意不要反编译 Microsoft Reference Source 中已包含的程序集,因为它们提供了注释更丰富的源代码。
要调试设计时期的演示解决方案,请将控件库“EncapsulatedDesigner
”设为启动项目。在其项目属性页面上,指向您的 devenv.exe 安装,并指定“TestDesigner
”项目的路径作为命令行参数。
到目前为止,我已将此处介绍的封装技术用于另外两个控件,它们深入利用了设计时基础设施,并且一切正常,但“您的体验可能会有所不同”。
历史
- 2011年1月24日:初始版本