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,並且保證從SiteCommandSet裡面取出的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的目的,具體實作和上面差不多,就不再寫了。