编写组件自己的设计器

专栏作品

 

编写组件自己的设计器
刘志波

Shawn Burke 微软公司 2001年6月 英文原稿 《Writing Custom Designers for .NET Components》 http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dndotnet/html/custdsgnrdotnet.asp 刘志波 译 2001年12月 摘要:这篇文章包括了设计器的各种特性,如何把他们和组件关联起来,如何使用这些特性来创造更强的设计时期用户界面。 内容: 简介 什么是设计器? 组件如何和设计器关联? 修改设计时期的状态信息 定义组件之间的关系 使用组件自己的设计器来更改组件的属性、特性、事件 简化通常要做的工作 与其他组件精确的交互 结论

简介

.NET框架是建造在大脑思维的扩展能力上的。由于.NET框架运行时期与设计时期的设计和实现是同一组工程师,用户就可以得到比其他的框架或者类库更加紧密地综合性能。 这种综合能力的一个关键因素就是基于代码的运行时期和设计时期的相互作用。由于运行时期的代码可以与设计时期的代码分开,设计部分就可以致力于在设计时期组件的行为和表现上施加相当多的注意力。 首先,我们来讨论一下词组“设计器(Designer)”的用法。通常意义下,他意味着在任何可以管理组件运行时期行为的.NET对象。不过有时更加广义的指在Microsoft® Visual Studio® .NET中设计Windows® Forms, Web Forms, 或者Components时的设计时期界面。这篇文章里“设计器”指某个特定组件的自己的设计器而不是广义的设计器,除非我们特别说明。 这篇文章会讲解设计器的各种特性,如何把他们和组件关联起来,如何使用这些特性来创造更强的设计时期用户界面。

什么是设计器?

就像上面提到的,设计器是负责管理设计界面上的组件的设计时期行为和表现的对象。特别的,设计器就是指实现了System.ComponentModel.Design.IDesigner接口的对象。

public interface IDesigner : IDisposable {

        IComponent Component {get;}       

        DesignerVerbCollection Verbs {get;}

        void DoDefaultAction();

        void Initialize(IComponent component);

}

一般的,不必要从头开始写一个设计器。.NET框架SDK中所有的设计器都是继承自System.ComponentModel.Design.ComponentDesigner的默认实现。任何实现IComponent的对象(一般是继承自Component)都会自动的得到ComponentDesigner作为他的设计器。同样有一些实现System.ComponentModel.Designer.IRootDesigner的其他类型的设计器,我们把他们叫做“根设计器(Root Designer)”,他可以允许一个对象成为Visual Sudio .NET设计环境中的“根(root)”对象。像System.Windows.Forms.Form和System.Windows.Forms.UserControls这些类型就有根设计器,这样在VS.NET中就会有设计视图和代码视图。一个类型可以有多个关联的设计器,不过每一个设计器都只能是一种类型。在这篇文章里,我们会探索标准IDesigner实现的特性。 大部分设计器执行3个基本工作: l创建和修改组件的设计时期界面 l修改组件提供的公开的属性(Properties)、特性(Attributes)、事件(Events) l增加叫做动词(Verbs)的动作,这些动作可以在组件的设计时期执行 在设计时期,.NET设计器基础架构会把一个设计器关联到每一个驻扎(Sited)的组件上。就是说,每一个组件会得到一个名字和允许在设计服务下访问的连接。然后每一个设计器比如VS.NET设计器就可以有能力和用户进行交互,可以操作代码生成和持久保持(持久性)。

组件如何和设计器关联?

元数据(与类、属性、事件、方法联系在一起的信息)的能力和适应性在整个.NET框架中都得到使用,设计器同样会使用这些重要信息。设计器使用System.ComponentModel.DesignerAttribute来和组件关联。这个特性的构建器的参数可以使用指向assembly的字符串类型名字或者是一个实际的类型引用。字符串形式是很有用的,因为它可以保证组件的设计时期和运行时期完全分开。由于运行时期和设计时期的代码在分开的集合(assemblies)中,组件卖主可以最小化运行时期的内存分配。相反的,如果设计器包含在同一个assembly中或者总可以得到时,DesignerAttribute也可以使用实际的类型引用参数。使用字符串形式同样可以避免循环依赖编译,不过由于设计器类型编译时期就得到,所以我们这里的例子都使用简化的类型引用方式。 举个例子来说,如果一个叫做MyCompany.SysComponent组件在MyCompany.dll中,而叫做MyCompany.Design.SysComponentDesign在MyCompany.Design.dll中:

namespace MyCompany {

[Designer("MyCompany.Design.SysComponentDesigner, MyCompany.Design")]

public class SysComponent : Component {

}

}

namespace MyCompany.Design {

   internal class SysComponentDesigner : ComponentDesigner {

      // …

}

}

当然,设计器同样可以与组件包括在同一个集合中(assembly),比如是嵌入的类,而且可以使用internal、public、protected修饰符。

namespace MyCompany {

[Designer(typeof(SysComponentDesigner))]

public class SysComponent : Component {

  

internal class SysComponentDesigner : ComponentDesigner{

// …

}

}

}

使用实际类型引用的好处就是当引用错误的时候编译器就会告诉我们,而字符串名字却不会告诉我们错误。 System.ComponentModel.Design.IDesignerHost接口允许设计界面上的组件访问设计器。使用一个IServiceProvider(比如在设计时期通过一个IComponent的Site属性得到的ISite),任何组件的设计器都可以通过IDesignerHost.GetDesigner方法访问。这里的代码来访问一个给定组件的动词(Verbs)。我们会在这篇文章的后面来讨论动词。

public DesignerVerbCollection GetComponentVerbs(IComponent comp) {

        if (comp.Site != null){

            IDesignerHost host;

            host = comp.Site.GetService(typeof(IDesignerHost));

            if (host != null){

                IDesigner designer;

                designer = host.GetDesigner(comp);

                if (designer != null){

                    return designer.Verbs;

                }

            }

        }

        return new DesignerVerbCollection();

}

修改设计时期的状态信息

在很多情况下,让一个控件或者组件在设计时期的行为与运行时期的行为一样是没有什么实际需要的。比如,一个时间控件在设计时期就不会执行时间事件,一个系统监督控件不会勾出(hook)系统事件。设计器有一种简单的处理方式。 举一个例子,假设一个用户控件可以有拖放输入行为,就比如是RichEdit。很明显,在设计时期托一个文件或者一些文本到这个控件上会产生歧义。为了防止这种现象,一个设计器可以禁止掉控件的拖放支持。

public class DragDropControlDesigner : ControlDesigner {

      public override void Initialize(IComponent c) {

         base.Initialize(c);

         ((Control)c).AllowDrop = false;

      }

   }

这个时候,设计器设置控件的AllowDrop属性为false。注意在自己的代码前面调用基类的Initialize方法。这是很重要的,基类的Initialize方法在设计器可以访问之前会设置基类的一些状态。同时也要注意到Initialize方法的参数是一个IComponent,这是要被设计的组件对象的实例。可以通过基于ComponentDesigner的设计器的ComponentDesigner.Component属性来访问。如果你是基于ControlDesigner写一个设计器,他同样有一个叫Control的属性让你可以访问在设计的Control。很多情况,这个返回值和ComponentDesigner.Component的返回值是一样的,不过他保存了任何时候的类型转换。理论上,你自然是可以使用这点来给一个非控件写一个基于ControlDesigner的设计器,然后重载Control这个属性返回给UI一个组件。 ControlDesigner像上面的例子一样执行了好几个步骤。由于设计器操作了生动的组件实例界面设计,因此一个控件必须是可见而且为了可以在设计界面上操作必须是允许操作的。如果不是这样的话,这个空间要么是看不见的,要么是不能够正常地接受鼠标和键盘输入,因此也就不能够移动。所以,在ControlDesigner.Initialize方法里,Visible和Enabled必须设置为true。

定义组件之间的关系

一些组件有他的关联组件,这些关联组件要么是一起显示要么是一起不显示在设计界面上。比如,ToolBar控件的按钮就是实际的组件自己。TabControl的TabPages也是一样的。如果你要把一个ToolBar从一个form复制到另一个form,就不能够只复制ToolBar对象本身,而把其他的那些对象留下来不管。这种场合同样适合于GroupBox和MainMenu控件。那么,如何用一种通用的方式来解决这个问题呢? 在ComponentDesigner上,有一个叫AssociatedComponents属性就是用来解决这个问题的。无论什么时候,一个拖放或者是复制/粘贴操作作用在组件上,VS.NET设计器就会循环调用AssociatedComponents的每一个组件的设计器来决定要拖放、复制、粘贴的全部对象。 在下面的这个例子里,MainComp把他所有的SubComp作为AssociatedComponent来返回。SubComp组件只是简单的返回驻扎的(site)VS.NET设计器。组件有时根据非设计器生成元素的集合项来初始化他们的状态。如果一个MainComp被复制到另一个form中或者是组件设计器中,他同时会复制所有的SubComp。想一想,这是多么让人激动啊。

[Designer(typeof(MainComp.MainCompDesigner))]

public class MainComp : Component {

 

   public SubCompCollection SubComponents {

      get {  

         return subCollection;

      }

   }

 

   internal class MainCompDesigner : ComponentDesigner {

      public override ICollection AssociatedComponents{

         get{

            return ((MainComp)base.Component).SubComponents;

         }

      }

   }

}

 

[DesignTimeVisible(false)]

[Designer(typeof(SubComp.SubCompDesigner))]

public class SubComp : Component {

   public SubCompCollection SubSubComponents {

      get {  

         return subCollection;

      }

   }

   internal class SubCompDesigner : ComponentDesigner {

 

      public override ICollection AssociatedComponents{

         get{

            // Only return sited subs in this case. 

            // For example, this component could have

            // a number of sub components that were

            // added by the component and aren't sited

            // on the design surface.  We don't want

            // to move those around.

            //

            ArrayList comps = new ArrayList();

            foreach (SubComp sc in

               ((SubComp)Component).SubSubComponents) {

               if (sc.Site != null) {

                  comps.Add(sc);

               }

            }

            return comps;

         }

      }

   }

}

使用组件自己的设计器来更改组件的属性、特性、事件

一个一般的设计器应用就是如何来调整控件在设计界面上的表现。有很多场合我们需要在组件的设计时期修改或者添加他的属性。VS.NET设计器已经为设计器上的每一个组件添加了很多属性,比如(Name)属性或者是Locked属性。这些属性在组件的属性中并不真正的存在的。属性的特性同样也是可以修改的。大部分时候,设计器应该可以中途截获或者是把组件的某些属性影子化(shadowing)。通过影化(shadowing)一个组件,设计器可以跟踪用户设置的值并且来决定是否把这个改变传给实际的组件。 当我们使用Control.Visible和Control.Enabled情形时,可以让控件总是可以看见并且是可以使用的。又或者是在Timer.Enabled情形,可以不让Timer控件被唤醒而且执行时间事件。在设计时期这些属性都是可以使用的,而且不会影响到设计界面上控件的状态。这种影化(shadowing)可以在基于ComponentDesigner类的设计器上很好的实现。 首先,ComponentDesigner有三类方法来修改被编辑组件暴露的属性。 l PreFilterProperties l PostFilterProperties l PreFilterAttributes l PostFilterAttributes l PreFilterEvents l PostFilterEvents 要遵守的一般原则就是在PreFilter等方法中添加或者移去某些项(items),并且在PostFilter方法中修改已经存在的项。在PreFilter方法中一定要首先调用基类方法,而在PostFilter方法中要在最后调用基类方法。这个保证了所有设计器有能力来应用他们的改变。ComponentDesigner同样有一个内嵌的保存被作影子属性(shadowed)值的字典(dictionary)。这个为设计器保存了当为属性创造成员时出现的问题。 我们就举一个简化的ComponentDesigner版本来看看。这个设计器会隐藏(shadow)掉Visible和Enabled属性,而且增加了Locked属性。

public class SimpleControlDesigner : ComponentDesigner {

 

   bool locked;

 

   public bool Enabled {

      get {

         return (bool)ShadowProperties["Enabled"];

      }

      set {

         // note this value is not passed to the actual

         // control

         this.ShadowProperties["Enabled"] = value;

      }

   }

   private bool Locked {

      get {

         return locked;

      }

      set {

         locked = value;           

      }

   }

 

   public bool Visible {

      get {

         return (bool)ShadowProperties["Visible"];

      }

      set {

         // note this value is not passed to the actual

         // control

         this.ShadowProperties["Visible"] = value;

      }

   }

 

   public void Initialize(IComponent c) {

      base.Initialize(c);

      Control control = c as Control;

 

      if (control == null) {

         throw new ArgumentException();

      }

 

      // pick up the current state and push it

      // into our shadow props to initialize them.

      //

      this.Visible = control.Visible;

      this.Enabled = control.Enabled;

 

      control.Visible = true;

      control.Enabled = true;

   }

 

protected override void PreFilterProperties(IDictionary properties) {

        base.PreFilterProperties(properties);

       

      // replace Visible and Enabled with our shadowed versions.

      //

      properties["Visible"] = TypeDescriptor.CreateProperty(

               typeof(SimpleControlDesigner),

               (PropertyDescriptor)properties["Visible"],

               new Attribute[0]);

      properties["Enabled"] = TypeDescriptor.CreateProperty(

               typeof(SimpleControlDesigner),

               (PropertyDescriptor)properties["Enabled"],

               new Attribute[0]);

 

      // and add the Locked property

      //

      properties["Locked"] = TypeDescriptor.CreateProperty(

               typeof(SimpleControlDesigner),

               "Locked",

               typeof(bool),

               CategoryAttribute.Design,

               DesignOnlyAttribute.Yes);

      }

}

注意Initialize方法是如何被用来生成组件的影子(shadow)属性的。而且Locked属性有DesignOnlyAttribute.Yes这个参数,并且是私有的。虽然他被标记为私有,不过还是可以通过reflection来访为,这是因为从访问代码到属性的连接已经建立了。DesignOnlyAttribute.Yes标记这个属性仅仅是在设计时期有效,所以他不会为组件来生成代码(设计时期属性持久性信息保存在资源中)。 TypeDescriptor.CreateProperty方法要么生成一个新的PropertyDescriptor要么是一个已有的PropertyDescriptor。由于属性都是定在在SimpleControlDesigner类里,typeof(SimpleControlDesigner)被指定为每一个生成的属性的组件。这个告诉运行期(runtime)当设置或者访问属性的时候哪一种类型的对象实例可以使用。一个忠告:在自己继承设计器时,一定不要使用GetType()来代替静态类型表达式。在导出类里,GetType()返回一个不同的值而且在访问属性时会引起问题。 一旦在设计界面上选择了组件,显示在属性窗口的就是这个组建暴露出来的属性。在设计时期影子属性隐藏了实际的Visible和Enabled属性,同时也包括代码生成和对象的持久信息。因此我们就有能力来提供用户希望的值而不是设计界面上组件的实际属性。

简化通常要做的工作

如果你的组件中有一些要执行的常用动作,我们一般把他们作为一种动词(verb)暴露出来。看看实际的动作,拖一个TabControl到窗体上就可以在属性窗口下方见到一些链接。一个叫做Add Tab另一个就叫做Remove Tab。正如这些名字所说的那样,点击他们就有添加或者是删除Tab的动作。同时我们也可以在右击控件所显示的快捷菜单上看到这些动作。 增加动作很简单。IDesigner接口有可以重载的verbs属性而且返回一个集合对象DesignerVerb。DesignerVerbs对象包括动作字符串名字,调用动作所使用的代理,和一个可选的命令ID(Command ID)。正常情况下,VS.NET设计器架构动态的给动作一个ID。举一个例子来说,下面是一个继承自Button类对象的设计器。他添加了三个动作:Red、Green、Blue,动作会改变组件的背景色彩。注意ControlDesigner有一个叫Control的属性会返回设计器联系的控件。

[Designer(typeof(ColorButton.ColorButtonDesigner))]

   public class ColorButton : System.Windows.Forms.Button

   {

       internal class ColorButtonDesigner : ControlDesigner {

         private DesignerVerbCollection verbs = null;

 

    private void OnVerbRed(object sender, EventArgs e) {

         Control.BackColor = Color.Red;

      }

 

      private void OnVerbGreen(object sender, EventArgs e){

         Control.BackColor = Color.Green;

      }

 

      private void OnVerbBlue(object sender, EventArgs e) {

         Control.BackColor = Color.Blue;

      }

 

      public override DesignerVerbCollection Verbs {

         get {

           if (verbs == null) {

            verbs = new DesignerVerbCollection();

            verbs.Add( new DesignerVerb(

                         "Red",

                         new EventHandler(OnVerbRed)));

            verbs.Add( new DesignerVerb(

                         "Blue",

                         new EventHandler(OnVerbBlue)));

            verbs.Add( new DesignerVerb(

                         "Green",

                         new EventHandler(OnVerbGreen)));

             }

           return verbs;

         }

      }

      }

   }

与其他组件精确的交互

当设计器改变了一个组件的状态时,其他的一些组件可能会对这个改变有兴趣。举一个例子,如果你改变了一个组件的背景为红色,属性窗口会显示这个改变信息。一般的,组件状态的改变会通过IComponentChangeService广播出去。VS.NET设计器中的其他服务会监听这个IComponentChangeService,包括代码持久性引擎,redo/undo工具,属性窗口和对这个更新有兴趣的属性的更新状态。 IComponentService在一个组件将要改变和改变后都会得到注意。任何客户都会收到两个主要的通知信息:OnComponentChanging和OnComponentChanged。OnComponentChanging必须在OnComponentChanged之前触发,而OnComponentChanged并不需要在IComponentChanging之后调用。当由于某种原因取消动作时这个就可以起作用。IComponentChangeService有好几个监听通知的事件。 l ComponentChanging l ComponentChanged l ComponentAdding l ComponentAdded l ComponentRemoving l ComponentRemoved l ComponentRename 服务仅仅允许ComponentChanging和ComponentChanged手动触发。其他的事件都是设计器在组件添加、删除、更改名字的时候自动触发的。 当通过PropertyDescriptor改变一个属性的值时,组件通知信息自动就发送出去,也就是这是一个相当简便的方式来做一些通知工作。 在上面的例子里,如果我们用PropertyDescriptor来通知改变(属性窗口会自动更新),我们把动作处理的代码改成如下:

private void OnVerbGreen(object sender, EventArgs e){

PropertyDescriptor backColorProp =

      TypeDescriptor.GetProperties(Control)["BackColor"];

  

   if (backColorProp != null) {

      backColorProp.SetValue(Control, Color.Green);

}

}

同样有一些情况是改变一个属性的值会影响到其他的一些属性,或者是一些情况是许多属性的改变同时发生。考虑到性能因素,直接修改对象的属性值(而不是使用PropertyDescriptor),然后接着调用通知。用PropertyDescriptor来访问属性会比直接来得慢。 我们看一下RadioButton。当改变一个RadioButton的值为true时,其他的有同一个parent对象的RadioButton就应该设置为false。运行期代码会自动处理这些,不过设计时期还是要自己来处理比较好。在这个例子里,我们把Checked属性影化来中途截取改变的值。当值设为true时,我们就循环处理RadioButton的兄弟,并且通知每一个RadioButton的IComponentService来改变他们的值。我们通过调用ComponentDesigner的GetService方法来得到一个IComponentService对象句柄然后调用他的实例。

internal class RadioButtonDesigner : ControlDesigner {

 

   public bool Checked  {

      get {

         // pass the get down to the control.

         //

         return ((RadioButton)Control).Checked;

      }

      set {

         // set the value into the control

         //

         ((RadioButton)Control).Checked = value;

 

         // if the value for this radio button

         // was set to true, notify that others have changed.

         //

         if (value) {

            IComponentChangeService ccs =

               (IComponentChangeService)

            GetService(typeof(IComponentChangeService));

            if (ccs != null) {

               PropertyDesciptor checkedProp =

                   TypeDescriptor.GetProperties(typeof(RadioButton))["Checked"];

               foreach(RadioButton rb in

                  Control.Parent.Controls) {

                  if (rb == Control || !(rb is RadioButton))

continue;

                 

                  ccs.OnComponentChanging(rb, checkedProp);

                  ccs.OnComponentChanged(rb,chedkedProp, null, null);

               }

            }

         }

      }

   }

 

   protected override void PreFilterProperties(

                     IDictionary properties) {

      base.PreFilterProperties(properties);

      // shadow the checked property so we can intercept the set.

      //

      properties["Checked"] =

TypeDescriptor.CreateProperty(               typeof(RadioButtonDesigner),

               (PropertyDescriptor)properties["Checked"],

               new Attribute[0]);

   }

}

结论

设计器可以给任何实现IComponent的对象编写并且驻扎(sited)在设计界面上。这对Windows Forms Controls、Web Form Controls和其他的组件来说都是适用的。给组件订制的设计器可以有不同于标准的设计界面而且可以让设计时期代码和执行时期代码分离,减少发布文件大小而且可以更强的控制组件的设计期行为。 从扩展性能上说,从.NET框架的设计时期接口和类导出的对象有能力订制自己的设计期行为,而以前的框架没有这些。运用这些能力来扩展组件就可以有更加丰富的设计期行为和运行期对象模型。

 

 

你可能感兴趣的:(编写组件自己的设计器)