实现类似“添加扩展程序…”的设计时支持

2/25/2010来源:ASP技巧人气:4889

  Ajax Control Toolkit这个控件库内包含一些扩展控件,利用这些扩展控件,可以非常方便的为普通的控件添加Ajax效果,例如,利用AutoCompleteExtender控件,可以为文本框添加自动完成的ajax效果。当然,这并不是本文想讨论的内容。

    将Ajax Control Toolkit加入到Visual Studio 2008的工具箱中,并打开一个新的aspx文件,向里面拖入一个TextBox。这时,有趣的事情发生了,在TextBox的SmartTasks面板里,竟然出现了一个“添加扩展程序…”的链接!我又试着拖入一个Button,一个Panel,无一例外的,每个控件的SmartTasks面板的底部都出现了“添加扩展程序…”的链接。

    最近我正打算把保存、删除、关闭页面等功能抽象成动作,每一种动作对应一个自定义的Web控件,将某个动作控件附加到目标控件(例如Button)上面之后,目标控件就拥有了诸如保存、删除、关闭页面的功能。如何在WebForm设计器里为一个Button控件方便地附加动作?我想要的正是类似“添加扩展程序…”这样的效果。

    开发过自定义服务器控件的朋友应该知道,如果想给控件添加SmartTasks,需要重写ControlDesigner的ActionLists属性,并实现自己的DesignerActionList。显然,一个TextBox并不知道AjaxControlToolkit的存在,所以“添加扩展程序…”这么一个DesignerActionMethodItem并不是它加进来的。那么,.net framework是否提供了某种接口,可以让我们为别的控件“动态的注入”DesignerActionItem呢?

    通过对AjaxControlToolKit.dll的研究,我发现这些扩展控件的Designer并不负责提供“添加扩展程序”这个Action,他们只负责提供相应扩展程序对应的扩展内容,所以只能从Visual studio的webform designer作为入口来研究。用reflector打开Microsoft Visual Studio 9.0\Common7\IDE\Microsoft.Web.Design.Client.dll,找到了IWebSmartTasksPRovider接口,该接口有一个GetDesignerActionLists的方法,这个方法的返回值应该就是SmartTasks面板里显示的内容了。这个接口有3个实现类,DataFormDesigner、DataFormXslValueOfDesigner、ElementDesigner。从这三个类的命名上可以推断,ElementDesigner应该是用的最多的实现类了。ElementDesigner的GetDesignerActionLists的方法实现如下:

   1: DesignerActionListCollection IWebSmartTasksProvider.GetDesignerActionLists()
   2: {
   3:     DesignerActionListCollection componentActions = null;
   4:     if (this.Designer != null)
   5:     {
   6:         DesignerActionService service = (DesignerActionService) base.DesignerHost.GetService(typeof(DesignerActionService));
   7:         if (service != null)
   8:         {
   9:             componentActions = service.GetComponentActions(this.Designer.Component);
  10:         }
  11:     }
  12:     if (componentActions == null)
  13:     {
  14:         componentActions = new DesignerActionListCollection();
  15:     }
  16:     return componentActions;
  17: }
  18:  
  19:  
  20:  
  21:  

    从上面代码里可以看到最终的DesignerActionListCollection是由System.Design程序集下的System.ComponentModel.Design.DesignerActionService类的GetComponentActions决定的,Microsoft.Web.Design.Client.dll下的Microsoft.Web.Design.WebFormDesigner+WebDesignerActionService继承了该类,他的实现如下:

   1: protected override void GetComponentDesignerActions(IComponent component, DesignerActionListCollection actionLists)
   2: {
   3:     Control control = component as Control;
   4:     ElementDesigner parent = null;
   5:     if (control != null)
   6:     {
   7:         parent = ElementDesigner.GetElementDesigner(control);
   8:     }
   9:     if ((parent == null) || !parent.InTemplateMode)
  10:     {
  11:         base.GetComponentDesignerActions(component, actionLists);
  12:         if ((parent != null) && (parent.Designer != null))
  13:         {
  14:             ControlDesigner designer = parent.Designer as ControlDesigner;
  15:             if ((designer != null) && (designer.AutoFormats.Count > 0))
  16:             {
  17:                 actionLists.Insert(0, new AutoFormatActionList(parent));
  18:             }
  19:         }
  20:         if ((parent != null) && (parent.Element != null))
  21:         {
  22:             IWebDataFormElementCallback dataFormElementCallback = parent.Element.GetDataFormElementCallback();
  23:             if (dataFormElementCallback != null)
  24:             {
  25:                 DataFormElementActionList list = new DataFormElementActionList(parent, parent.Control, dataFormElementCallback);
  26:                 actionLists.Add(list);
  27:                 DataFormElementActionList.ModifyActionListsForListControl(actionLists, list);
  28:             }
  29:         }
  30:         if (((parent != null) && (parent.Designer != null)) && parent.DocumentDesigner.ExtenderControlHelper.ProvidesActionLists)
  31:         {
  32:             parent.DocumentDesigner.ExtenderControlHelper.AddActionLists(parent, actionLists);
  33:         }
  34:     }
  35:     if ((parent != null) && (parent.TemplateEditingUI != null))
  36:     {
  37:         actionLists.Add(new TemplateEditingActionList(parent.TemplateEditingUI, parent.Element));
  38:     }
  39: }
  40:  
  41:  
  42:  
  43:  

    这个方法里,有这么一段:

   1: if (((parent != null) && (parent.Designer != null)) && parent.DocumentDesigner.ExtenderControlHelper.ProvidesActionLists)
   2:        {
   3:            parent.DocumentDesigner.ExtenderControlHelper.AddActionLists(parent, actionLists);
   4:        }


    看来“添加扩展程序”这个action就是在这里加进去的了。继续查看ExtenderControlHelper.AddActionLists的实现:

   1: public void AddActionLists(ElementDesigner element, DesignerActionListCollection lists)
   2: {
   3:     lists.Add(new ControlExtenderActionList(element));
   4:     ExtenderControl component = element.Designer.Component as ExtenderControl;
   5:     Control control = element.Designer.Component as Control;
   6:     if ((component == null) && (control != null))
   7:     {
   8:         IExtenderInformationService service = (IExtenderInformationService) control.Site.GetService(typeof(IExtenderInformationService));
   9:         if (service != null)
  10:         {
  11:             foreach (Control control3 in service.GetAppliedExtenders(control))
  12:             {
  13:                 lists.Add(new HoistedExtenderActionList(element.Designer, control3));
  14:             }
  15:         }
  16:     }
  17: }
  18:  
  19:  
  20:  
  21:  


    这个方法里的第一句是lists.Add(new ControlExtenderActionList(element)),ControlExtenderActionList继承了System.ComponentModel.Design.DesignerActionList,他的GetSortedActionItems方法定义如下:

   1: public override DesignerActionItemCollection GetSortedActionItems()
   2: {
   3:     Control component = (Control) this._htmlDesigner.Component;
   4:     DesignerActionItemCollection items = new DesignerActionItemCollection();
   5:     IExtenderInformationService service = (IExtenderInformationService) component.Site.GetService(typeof(IExtenderInformationService));
   6:     string category = SR.GetString(SR.Ids.SmartTasksLabelExtenderSection, CultureInfo.CurrentUICulture);
   7:     if (service.IsControlExtendible(component))
   8:     {
   9:         string displayName = SR.GetString(SR.Ids.SmartTasksAddExtender, CultureInfo.CurrentUICulture);
  10:         items.Add(new DesignerActionMethodItem(this, "AddExtender", displayName, category, true));
  11:     }
  12:     if (service.IsControlExtended(component))
  13:     {
  14:         string str3 = SR.GetString(SR.Ids.SmartTasksRemoveExtender, CultureInfo.CurrentUICulture);
  15:         items.Add(new DesignerActionMethodItem(this, "RemoveExtender", str3, category, true));
  16:     }
  17:     return items;
  18: }
  19:  

    这下清楚了,“添加扩展程序”这个action,是在Visual studio的web form设计器里,写死进去的,.net framework并没有提供相应接口来供我们添加类似的action。但是我想要的效果是增加一个“添加动作”的action,所以我不能参考AjaxControlToolkit的方法去实现,应该要寻找别的方法。

    回过头来,重新查看Microsoft.Web.Design.WebFormDesigner+WebDesignerActionService类的GetComponentActions方法,找到基类System.Web.UI.Design.WebFormsDesignerActionService(在System.Design程序集下)的定义,如下:

   1: protected override void GetComponentDesignerActions(IComponent component, DesignerActionListCollection actionLists)
   2: {
   3:     if (component == null)
   4:     {
   5:         throw new ArgumentNullException("component");
   6:     }
   7:     if (actionLists == null)
   8:     {
   9:         throw new ArgumentNullException("actionLists");
  10:     }
  11:     IServiceContainer site = component.Site as IServiceContainer;
  12:     if (site != null)
  13:     {
  14:         DesignerCommandSet service = (DesignerCommandSet) site.GetService(typeof(DesignerCommandSet));
  15:         if (service != null)
  16:         {
  17:             DesignerActionListCollection lists = service.ActionLists;
  18:             if (lists != null)
  19:             {
  20:                 actionLists.AddRange(lists);
  21:             }
  22:         }
  23:         if ((actionLists.Count == 0) || ((actionLists.Count == 1) && (actionLists[0] is ControlDesigner.ControlDesignerActionList)))
  24:         {
  25:             DesignerVerbCollection verbs = service.Verbs;
  26:             if ((verbs != null) && (verbs.Count != 0))
  27:             {
  28:                 DesignerVerb[] array = new DesignerVerb[verbs.Count];
  29:                 verbs.CopyTo(array, 0);
  30:                 actionLists.Add(new DesignerActionVerbList(array));
  31:             }
  32:         }
  33:     }
  34: }
  35:  
  36:  
  37:  
  38:  

    通过研究上述代码,可以看到DesignerActionListCollection是由DesignerCommandSet这个service的ActionLists属性负责返回的,而这个service是从component的Site里面取得的,只要我另外写一个DesignerCommandSet,并且保证从Site里面取出的DesignerCommandSet是我写的这个service就可以了。终于找到了切入点,下面是具体做法。

    首先,创建一个类继承DesignerCommandSet,如下:

   1: public class MyDesignerCommandSet : DesignerCommandSet
   2:     {
   3:         private ComponentDesigner _componentDesigner;
   4:  
   5:         public MyDesignerCommandSet(ComponentDesigner componentDesigner)
   6:         {
   7:             _componentDesigner = componentDesigner;
   8:         }
   9:  
  10:         public override ICollection GetCommands(string name)
  11:         {
  12:             if (name.Equals("ActionLists"))
  13:             {
  14:                 return GetActionLists();
  15:             }
  16:             return base.GetCommands(name);
  17:         }
  18:  
  19:         private DesignerActionListCollection GetActionLists()
  20:         {
  21:             //先取得控件原有的DesignerActionLists
  22:             DesignerActionListCollection lists = _componentDesigner.ActionLists;
  23:             
  24:             //增加“添加动作”这个DesignerActionList
  25:             lists.Add(new ActionList(_componentDesigner));
  26:             return lists;
  27:         }
  28:  
  29:         internal class ActionList : DesignerActionList
  30:         {
  31:             private DesignerActionItemCollection _actions;
  32:  
  33:             public ActionList(IDesigner designer)
  34:                 : base(designer.Component)
  35:             {
  36:             }
  37:             public override DesignerActionItemCollection GetSortedActionItems()
  38:             {
  39:                 if (_actions == null)
  40:                 {
  41:                     const string actionCategory = "Actions";
  42:                     _actions = new DesignerActionItemCollection();
  43:                     _actions.Add(new DesignerActionMethodItem(this, "AddAction", "添加动作...", actionCategory, true));
  44:                 }
  45:                 return _actions;
  46:             }
  47:  
  48:             public void AddAction()
  49:             {
  50:                 //添加动作的逻辑,略
  51:             }
  52:         }
  53:     }


    下一步就是如何使component的Site这个ServiceProvider返回自己的这个service。方法是自己写一个Site,并使Component的Site变成自己写的SIte类的对象。

    自己写的Site类的定义如下:

   1: public class SiteProxy : ISite, IServiceContainer
   2:     {
   3:         private ISite _site;
   4:         private ComponentDesigner _designer;
   5:  
   6:         public SiteProxy(ISite site, ComponentDesigner designer)
   7:         {
   8:             _site = site;
   9:             _designer = designer;
  10:  
  11:         }
  12:  
  13:         #region ISite 成员
  14:  
  15:         public IComponent Component
  16:         {
  17:             get { return _site.Component; }
  18:         }
  19:  
  20:         public System.ComponentModel.IContainer Container
  21:         {
  22:             get { return _site.Container; }
  23:         }
  24:  
  25:         public bool DesignMode
  26:         {
  27:             get { return _site.DesignMode; }
  28:         }
  29:  
  30:         public string Name
  31:         {
  32:             get { return _site.Name; }
  33:             set { _site.Name = value; }
  34:         }
  35:  
  36:         #endregion
  37:  
  38:         #region IServiceProvider 成员
  39:  
  40:         public object GetService(Type serviceType)
  41:         {
  42:             object service = _site.GetService(serviceType);
  43:  
  44:             if (serviceType == typeof(DesignerCommandSet) && !(_designer.Component is ExtenderControl))
  45:             {
  46:                 if (service == null || !(service is MyDesignerCommandSet))
  47:                 {
  48:                     if (service != null)
  49:                     {
  50:                         RemoveService(typeof(DesignerCommandSet));
  51:                     }
  52:                     //返回自己写的DesignerCommandSet
  53:                     service = new MyDesignerCommandSet(_designer);
  54:                     AddService(typeof(DesignerCommandSet), service);
  55:                 }
  56:             }
  57:             return service;
  58:         }
  59:  
  60:         #endregion
  61:  
  62:         #region IServiceContainer 成员
  63:  
  64:         public void AddService(Type serviceType, ServiceCreatorCallback callback, bool promote)
  65:         {
  66:             (_site as IServiceContainer).AddService(serviceType, callback, promote);
  67:         }
  68:  
  69:         public void AddService(Type serviceType, ServiceCreatorCallback callback)
  70:         {
  71:             (_site as IServiceContainer).AddService(serviceType, callback);
  72:         }
  73:  
  74:         public void AddService(Type serviceType, object serviceInstance, bool promote)
  75:         {
  76:             (_site as IServiceContainer).AddService(serviceType, serviceInstance, promote);
  77:         }
  78:  
  79:         public void AddService(Type serviceType, object serviceInstance)
  80:         {
  81:             (_site as IServiceContainer).AddService(serviceType, serviceInstance);
  82:         }
  83:  
  84:         public void RemoveService(Type serviceType, bool promote)
  85:         {
  86:             (_site as IServiceContainer).RemoveService(serviceType, promote);
  87:         }
  88:  
  89:         public void RemoveService(Type serviceType)
  90:         {
  91:             (_site as IServiceContainer).RemoveService(serviceType);
  92:         }
  93:  
  94:         #endregion
  95:     }


    在这个Site的GetService方法中,判断要get的service类型,如果是DesignerCommandSet,就返回自己创建的MyDesignerCommandSet。

    下一步是如何使component的Site变成自己写的SiteProxy。一种方法是新增一种自定义控件,在该控件的ControlDesigner的Initialize方法中改变Container中其他控件的Site,只需要向WebForm中拖入该控件,就可以改变其他控件的Site;另外一种方法是写一个vs package,在package中捕获web form designer的相应事件。下面介绍第一种做法:

    新增一个继承自Control的控件,叫做ActionManager,这个控件不用添加任何功能,只需要为它制作ControlDesigner。它的ControlDesigner类主要代码如下:

   1: public class ActionManagerDesigner : ControlDesigner
   2:     {
   3:         private IDesignerHost _host;
   4:         private IDictionary<IComponent, ISite> _components;
   5:  
   6:         public override void Initialize(IComponent component)
   7:         {
   8:             base.Initialize(component);
   9:  
  10:             _components = new Dictionary<IComponent, ISite>();
  11:  
  12:             _host = GetService(typeof(IDesignerHost)) as IDesignerHost;
  13:             if (_host != null)
  14:             {
  15:                 //替换已有控件的Site
  16:                 ProcessComponent();
  17:  
  18:                 IComponentChangeService service =
  19:                     _host.GetService(typeof(IComponentChangeService)) as IComponentChangeService;
  20:                 if (service != null)
  21:                 {
  22:                     service.ComponentAdded += ComponentAdded;
  23:                     service.ComponentRemoving += ComponentRemoving;
  24:                 }
  25:             }
  26:         }
  27:  
  28:         #region ProcessComponent
  29:  
  30:         private void ProcessComponent()
  31:         {
  32:             ComponentCollection components = _host.Container.Components;
  33:             foreach (IComponent component in components)
  34:             {
  35:                 if (component is ActionControl)
  36:                     continue;
  37:                 ProcessComponentSite(component);
  38:             }
  39:         }
  40:  
  41:         #endregion
  42:  
  43:         #region 替换Site
  44:  
  45:         /// <summary>
  46:         /// 替换Component原来的Site,换成SiteProxy
  47:         /// </summary>
  48:         private void ProcessComponentSite(IComponent component)
  49:         {
  50:             ComponentDesigner designer = _host.GetDesigner(component) as ComponentDesigner;
  51:             _components[component] = component.Site;
  52:             component.Site = new SiteProxy(component.Site, designer);
  53:         }
  54:  
  55:         /// <summary>
  56:         /// 恢复Component原来的site
  57:         /// </summary>
  58:         /// <param name="component"></param>
  59:         private void RestoreComponentSite(IComponent component)
  60:         {
  61:             if (_components.ContainsKey(component))
  62:             {
  63:                 ISite site = _components[component];
  64:                 component.Site = site;
  65:                 _components.Remove(component);
  66:             }
  67:         }
  68:  
  69:         #endregion
  70:  
  71:         #region on Component Add, remove, change
  72:         
  73:         private void ComponentRemoving(object sender, ComponentEventArgs e)
  74:         {
  75:             if (e.Component is ActionControl)
  76:             {
  77:                 return;
  78:             }
  79:             //在删除Component的时候,要把他的Site属性还原回去,否则DesignerHost中还会保留原来的Site,
  80:             //这样再添加同名的Component的时候,会报“重复的组件名称”错误
  81:             RestoreComponentSite(e.Component);
  82:         }
  83:        
  84:  
  85:         private void ComponentAdded(object sender, ComponentEventArgs e)
  86:         {
  87:             if (e.Component is ActionControl)
  88:             {
  89:                 return;
  90:             }
  91:             ProcessComponentSite(e.Component);
  92:         }
  93:  
  94:         #endregion
  95:  
  96:         #region dispose
  97:  
  98:         protected override void Dispose(bool disposing)
  99:         {
100:             if (_host != null)
101:             {
102:                 IComponentChangeService service =
103:                     _host.GetService(typeof(IComponentChangeService)) as IComponentChangeService;
104:                 if (service != null)
105:                 {
106:                     service.ComponentAdded -= ComponentAdded;
107:                     service.ComponentRemoving -= ComponentRemoving;
108:                 }
109:             }
110:             base.Dispose(disposing);
111:         }
112:  
113:         #endregion
114:     }

   至此,只要把一个ActionManager控件拖入到web form designer中,就可以在其他控件的smart task面板上看到“添加动作…”这个链接了。但是这种方式需要在webform designer中放入额外的一个控件,该控件只在设计时有用,在运行时则无用,看起来比较奇怪,所以最好的做法是第二种做法,即开发一个vs package,在package的Initialize方法中,注册IDesignerEventService的DesignerCreated事件,进而通过IDesignerHost和IComponentChangeService达到更改控件Site的目的,具体实现和上面差不多,就不再写了。