自己開發窗體設計器----轉載 劉志波 譯

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框架的設計時期接口和類導出的對象有能力訂製自己的設計期行爲,而以前的框架沒有這些。運用這些能力來擴展組件就可以有更加豐富的設計期行爲和運行期對象模型。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章