實現類似「添加擴展程式…」的設計時支援
- 2019 年 10 月 5 日
- 筆記
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.0Common7IDEMicrosoft.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的目的,具體實現和上面差不多,就不再寫了。