可複用程式碼:元件的來龍去脈

周見智發表於2015-06-12

相關文章連結

程式設計之基礎:資料型別(一)

程式設計之基礎:資料型別(二)

高屋建瓴:梳理程式設計約定

動力之源:程式碼中的泵

難免的尷尬:程式碼依賴

重中之重:委託與事件

物以類聚:物件也有生命

可複用程式碼:元件的來龍去脈

平時常說的"元件"包含的範圍比較廣泛,一個程式集、一個連結庫甚至程式碼中的一個類都可以稱為"元件",而本章講到的"元件"僅指.NET程式設計過程中實現了System.ComponentModel.IComponent介面的型別,包含範圍相對比較狹隘。本章介紹了元件的定義及其作用、元件的兩種狀態(設計時與執行時),還講到了"容器-元件-服務模型"以及應用了該模型的"窗體設計器"(Form Designer),最後提到了元件中的一個分支:控制元件以及自定義控制元件的分類。

7.1 .NET中的元件

7.1.1 元件的定義

本章討論的元件與傳統意義中的元件不同。傳統上講,任何一個模組均可以稱為"元件",大到一個程式集或者一個動態連結庫,小到程式碼層面上的一個型別,這些都能稱之為元件,它們被當作一個整體,能對外提供特定的功能。

本章討論的元件範圍相對來說更狹隘。在.NET程式設計中,我們把實現(直接或者間接)System.ComponentModel.IComponent介面的型別稱為"元件",其餘都不是本章將要討論的範疇。IComponent介面的定義如下:

1 //Code 7-1
2 public interface IComponent : IDisposable
3 {
4        event EventHandler Disposed;
5        ISite Site { get; set; }
6 }

如上程式碼Code 7-1所示,IComponent介面實現了IDisposable介面,說明它具有IDisposable介面的特性(言下之意便是,我們必須按照第四章中講IDisposable介面那樣去定義元件)。另外它還包含一個Disposed事件,顧名思義,該事件會在元件Dispose時激發。IComponent介面還包含一個ISite型別的屬性,從該屬性的名稱基本上可以確定,該屬性跟定位有關,沒錯,它就是用來幫助元件定位的,後面將會介紹到該屬性。

.NET框架中有一個IComponent介面的預設實現:System.ComponentModel.Component型別。該型別預設實現了介面中的方法、事件以及屬性,框架中包含的其它預定義元件大部分均派生自該Component類。

7.1.2 Windows Forms中的元件

前面講到過,在.NET中只要實現了IComponent介面的型別均叫元件。整個.NET框架中有許許多多的型別實現了IComponent介面,因此它們均稱為元件。下圖7-1顯示了Windows Forms框架中包含常見元件的關係:

 

圖7-1 Windows Forms中元件之間的關係

如上圖7-1所示,在System.Windows.Forms名稱空間中,有許多元件均派生自Component類,我們需要注意到,Control型別也派生自Component型別,因此,Control類(及其派生類)均屬於元件。

這裡需要強調一下,元件和控制元件不是相等的,元件包含控制元件,控制元件只是元件中的一個分類。另外我們常見的System.Windows.Forms.Timer以及System.Windows.Forms.ToolTip等等嚴格上講不能稱之為"控制元件"(雖然我們已經習慣這樣稱呼)。

7.1.3 Windows Forms中的控制元件

控制元件的種類也有很多,本章中我們只討論Windows Forms中的控制元件,我們把派生自System.Windows.Forms.Control類的型別稱為"控制元件"。控制元件中包含有處理Windows訊息的功能(比如視窗過程,詳見第八章),因此控制元件都能以某一種方式在螢幕上呈現出來。在Winform程式設計中,窗體也屬於控制元件,我們可以看到窗體類(Form)也派生自Control類:

 

圖7-2 Windows Forms中控制元件之間的關係

如上圖7-2所示,所有的控制元件均派生自Control類,Control類又屬於元件,因此所有控制元件均具備元件的特性。

不管元件還是控制元件,它們都是可以重複使用的程式碼集合,都實現了IDisposable介面,都需要遵循第四章中講到的Dispose模式。如果一個型別使用了非託管資源,它實現IDisposable介面就可以,那為什麼.NET程式設計中又要提出元件的概念呢?對於這個問題,並沒有官方答案,不過從.NET框架的結構和微軟為開發者提供的視覺化開發環境(比如Visual Studio)來看,這樣做可以說完全是為了實現程式的"視覺化開發",也就是我們常說的"所見即所得"。在類似Visual Studio這樣的開發環境中,一切"元件"均可被視覺化設計,換句話說,只要我們定義的型別實現了IComponent介面,那麼在開發階段,該型別就可以出現在窗體設計器中,我們可以使用窗體設計器編輯它的屬性、給它註冊事件,它還能被窗體設計器中別的元件識別等等。具體介紹請參見本章接下來的幾節。

注:控制元件是.NET程式設計中的一個重點,它能夠提高程式開發效率。Asp.NET中的所有伺服器控制元件均繼承自System.Web.UI.Control類,它們之間的關係與Windows Forms中的控制元件結構類似。

7.2 容器-元件-服務模型

7.2.1 容器的另類定義

這裡討論的容器與我們所熟知的傳統容器不同。談到容器,我們或許會想到ArraList、Queue又或者HashTable等類,它們的共同特徵就是可以存放資料,更具體一點的描述就是:我們能將一個元素放入到容器內部。但本章討論的容器跟這些都不相同,它們沒有物理包含的意思,只是邏輯上包含元素。如果稱傳統容器為物理容器,那麼稱這種只是邏輯上包含元素的容器為邏輯容器。兩種容器的差別如下圖7-3:

 

圖7-3 物理容器和邏輯容器的區別

如上圖7-3所示,物理容器中可以存放元素,但是邏輯容器卻沒有空間上的限制,物理容器中的元素可以屬於另外一個邏輯容器。

注:物理容器強調空間上的包含與被包含。對於像QueueStatck或者ArrayList這樣的物理容器,如果是引用型別物件,存放在這些容器內部的大部分都是物件的引用,而物件例項本身則在容器外部。

.NET程式設計中,把所有實現(直接或間接)System.ComponentModel.IContainer介面的型別稱為邏輯容器(以下簡稱"容器")。其餘都不在本章討論的範疇之內,IContainer介面定義如下:

1 //Code 7-2
2 public interface IContainer : IDisposable
3 {
4       void Add(IComponent component);
5       void Add(IComponent component, string name);
6       void Remove(IComponent component);
7       ComponentCollection Components { get; }
8 }

如上程式碼Code 7-2所示,IContainer介面也實現了IDisposable介面,說明它具有IDisposable介面的特性(言下之意就是,我們必須按照第四章中講IDisposable介面那樣去定義容器)。另外它還包含幾個新增、移除元素的方法,以及一個元件集合成員。我們可以發現,不管是方法的引數還是集合元素型別,都是元件型別,可以猜測到,容器當然是為元件服務的。沒錯,本章討論到的容器只能邏輯包含元件,只有元件才能成為它的邏輯元素。

.NET框架中有一個IContainer介面的預設實現:System.ComponentModel.Container型別,該型別預設實現了IContainer介面中的方法以及屬性。

現在我們已經知道,容器包含的邏輯元素是元件,容器也實現了IDisposable介面,很顯然,我們可以透過容器統一管理各個邏輯元素的非託管資源。但容器對於元件來講,僅僅是為了更方便地管理元件的非託管資源嗎?如果是這樣,完全沒必要存在本章討論的這種容器,看來,容器和元件相結合起來,還有其它的作用。

7.2.2 容器與元件的合作

前面講到過,傳統容器中可以物理包含元素,但是這些元素之間是相互獨立、不能互相通訊的。也就是說,如果ArrayList中包含A和B兩個元素,那麼A是不知道B的存在,B也不知道A的存在,B也更不可能告訴A:我今年25歲。傳統容器僅僅是在空間上簡單地將資料組織在一起,並不能為資料之間的互動提供支援。而本章討論的邏輯容器,在某種意義上講,更高階,它能為元件(邏輯元素)之間的通訊提供支援,元件與元件之間不再是獨立存在的,此外,它還能直接給元件提供某些服務。下圖7-4顯示物理容器和邏輯容器分別與元素之間的關係:

 

圖7-4 容器與元素之間的關係

如上圖7-4所示,物理容器中的元素之間不能相互通訊,物理容器也不可能為內部元素提供服務,邏輯容器中的元件之間可以透過邏輯容器作為橋樑,進行資料交換,同時,邏輯容器還能給各個元件提供服務。

上面提到過"服務",所謂服務,就是指邏輯容器能夠給元件提供一些訪問支援,比如某個元件需要知道它所屬容器共包含有多少個元件,那麼它就可以向容器請求,容器為它返回一個獲取元件總數的介面。類似下圖7-5:

 

圖7-5 元件向容器請求服務

如上圖7-5所示,元件向容器請求"計數服務",容器給元件返回一個ICountService的介面,該介面專門負責計數相關的操作,元件可以使用該介面獲取當前容器中所有元件的總數。同理,元件還可以向容器請求其它型別的服務,只要容器可以提供。

到此,我們已經大概知道了邏輯容器存在的真正目的,那就是為所有屬於該容器的元件提供服務,使元件與元件之間能夠自由互動。那麼,容器和元件內部到底有著怎樣的實現,才會達到如此效果?在本章7.1.1小節中就提到過IComponent介面中有一個ISite型別的屬性,當時說是起到一個"定位"的作用,現在看來,元件與容器之間的紐帶就是它,元件透過該屬性可以與它所屬容器取得聯絡。

我們看一下ISite介面的預設實現Site型別內部結構:

 1 //Code 7-3
 2 private class Site : ISite, IServiceProvider
 3 {
 4         private IComponent component;
 5         private Container container;
 6         private string name;
 7         internal Site(IComponent component, Container container, string name);
 8         public object GetService(Type service); //NO.1
 9         public IComponent Component { get; } //NO.2
10         public IContainer Container { get; } //NO.3
11         public bool DesignMode { get; }
12         public string Name { get; set; }
13 }

如上程式碼Code 7-3所示,Site型別中包含容器Container以及元件Component屬性(NO.3和NO.2處),它兩正是分別代表當前元件以及該元件所屬容器。Site型別中還包含一個GetService方法(NO.1處),該方法是元件向容器請求服務的關鍵。

我們再來看一下IComponent介面的預設實現Component型別內部結構(不全):

 1 //Code 7-4
 2 public class Component : MarshalByRefObject, IComponent, IDisposable
 3 {
 4      private static readonly object EventDisposed;
 5      private EventHandlerList events;
 6      private ISite site;
 7      [Browsable(false),  EditorBrowsable(EditorBrowsableState.Advanced)]
 8      public event EventHandler Disposed;
 9      static Component();
10      public Component();
11      public void Dispose();
12      protected virtual void Dispose(bool disposing);
13      protected override void Finalize();
14      protected virtual object GetService(Type service); //NO.1
15      public override string ToString();
16      //
17 }

如上程式碼Code 7-4所示,Component類中包含一個GetService方法(NO.1處),該方法的作用就是向它所在容器請求服務(見圖7-5),所有Component的派生類均可以使用GetService請求服務。

最後再看一下IContainer介面的預設實現Container型別內部結構(不全):

 1 //Code 7-5
 2 public class Container : IContainer, IDisposable
 3 {
 4       private ComponentCollection components;
 5       private int siteCount;
 6       private ISite[] sites;
 7       private object syncObj;
 8       public Container();
 9       public virtual void Add(IComponent component);
10       public virtual void Add(IComponent component, string name);
11       protected virtual ISite CreateSite(IComponent component, string name);
12       public void Dispose();
13       protected virtual void Dispose(bool disposing);
14       protected override void Finalize();
15       protected virtual object GetService(Type service); //NO.1
16       public virtual void Remove(IComponent component);
17       private void Remove(IComponent component, bool preserveSite);
18       //
19 }

如上程式碼Code 7-5所示,Container類中也包含一個GetService方法,它專門為元件提供服務,注意它是一個虛方法,也就是說,如果我們從它派生出來一個新的容器,我們完全可以在新容器中重寫該虛方法,增加新的服務(Container容器預設不提供任何服務)。下面程式碼建立一個新容器,為元件提供計數服務:

 1 //Code 7-6
 2 public class MyContainer:Container
 3 {
 4     protected override object GetService(Type service) //NO.1
 5     {
 6         if(service == typeof(ICountService))
 7         {
 8             return new CountService(this); //NO.2
 9         }
10         return base.GetService(service);
11     }
12 }
13 public interface ICountService //NO.3
14 {
15     //
16     int GetAllComponentsCount();
17 }
18 class CountService:ICountService //NO.4
19 {
20     MyContainer _container;
21     public CountService(MyContainer container)
22     {
23         _container = container;
24     }
25     public int GetAllComponentsCount() //NO.5
26     {
27         //
28     }
29 }

如上程式碼Code 7-6所示,新容器MyContainer重寫了GetService方法(NO.1處),當元件請求計數服務時,GetService方法為元件返回一個ICountService介面(NO.2處),元件之後就可以透過該介面獲取當前容器中元件的總數目(NO.5處)。

上面可以看到,Component、Site以及Container三個型別均包含有獲取服務的方法GetService,現在我們可以整理一下,元件向容器請求服務的流程:

 

圖7-6 元件請求服務流程

如上圖7-6所示,元件內部在請求服務的時候,先要判斷自己是否在某一個容器中,如果不在容器中,那麼返回null;如果在容器中,則以Site作為橋樑(Site.GetService),去獲取容器中的服務(Container.GetService)。

注:容器將元件新增進來的時候(執行Container.Add),會初始化該元件的Site屬性,讓該元件與容器產生關聯,只有當這一過程發生之後,元件才能獲取容器的服務。有關ComponentSite以及Container型別的詳細資訊請使用反編譯工具檢視.NET原始碼。

在程式設計中,使用"容器-元件-服務"模型時,只要容器不變(意味著提供服務的規則不變),我們就可以根據需要開發出各種各樣的元件,元件像一個隨身碟,隨時可以插在容器上工作。最重要的是,它能透過容器訪問到容器中其它元件,容器這時候更像一個匯流排(Bus),如下圖7-7:

圖7-7 容器充當匯流排功能

如上圖7-7所示,在匯流排不變的情況下,意味著通訊協議不變(容器不變,那麼提供服務的規則也不變),各種擴充套件元件(Component派生類)均可以加入這條匯流排。

7.2.3 窗體設計器

首先我們應該搞清楚一件事情,只有原始碼經過編譯器編譯之後才會生成可執行檔案。在這個過程中,所有其它的開發工具都只是起到一個輔助作用,無論我們的原始碼是怎樣來的,是用記事本手動編寫還是透過某些工具自動生成,最後本質上都一樣,編譯器只認結果,並不關心原始碼是怎樣產生的。現在流行的一些整合開發環境(IDE)中基本都會帶有視覺化設計介面,通常稱作"窗體設計器",該窗體設計器的功能就是幫助我們自動生成原始碼。沒錯,我們點點滑鼠、拖拖控制元件,窗體設計器就會為我們生成對應的源程式,窗體設計器的作用如下圖7-8:

圖7-8 窗體設計器在開發中的作用

如上圖7-8所示,窗體設計器最終也是為了生成原始碼,本質上跟程式碼編輯器的作用一樣。

其次,我們同時也要清楚,我們向窗體設計器中新增的各個元件(比如Timer、BackgroundWorker、ImageList等等)以及各種控制元件(比如Button、Label以及Form等),它們到底是個什麼東西?我們向窗體設計器中拖進去的Button控制元件和程式跑起來之後顯示在窗體上的Button按鈕是一樣的嗎?答案是肯定的,也就是說,我們向窗體設計器中拖進去的控制元件(元件)都是記憶體中存在的物件例項。如果我們使用Spy++等工具,可以找到窗體設計中控制元件的控制程式碼,同時我們還可以透過屬性窗體(PropertyGrid控制元件)修改窗體設計器中控制元件(元件)的屬性,這些修改在設計器中立即就會產生效果,這些都足以說明,在我們向窗體設計器中拖動控制元件的時候,是會執行類似"new Button();"這樣的程式碼,在記憶體中例項化一個元件例項。那麼,是所有的型別均可以拖放到窗體設計器中嗎?答案是否定的,不難發現,能夠被窗體設計器設計的像Imagelist、Timer等以及所有的控制元件都屬於"元件",它們都派生自System.ComponentModel.Component型別。我們好像發現了什麼,沒錯,前面說到過,容器(System.ComponentModel.Container或其派生類)只能包含元件,並且能夠為元件提供服務,元件之間也能夠相互通訊,那麼,我們是不是也可以把窗體設計器當作這樣的一種容器呢?事實證明,窗體設計器確實是我們前面提到過了容器,我們在使用窗體設計器時,各個元件之間確實是可以相互通訊的,當我們往窗體設計器中拖放一個ImageList元件時,設計器中所有其它包含有ImageList型別屬性的控制元件都會知道這個拖放進來的ImageList元件,因此在我們編輯其它控制元件的ImageList型別屬性時,就會有選項供選擇。我們不僅僅知道當前可以用來賦值的ImageList元件,還可以看到ImageList元件中的Image列表,從而設定控制元件的ImageList屬性值。這一過程的主要貢獻在於窗體設計器,它能夠為元件與元件之間建立關聯。

微軟的Visual Studio開發環境中的窗體設計器很好的應用了本節之前講到"容器-元件-服務模型",這也印證了本章開始之前的一個疑問:.NET程式設計中為什麼要提出元件的概念?由於窗體設計器中只能容納元件,所以如果我們想開發出來一個可以被窗體設計器視覺化設計的型別,那麼我們必須讓該型別正確實現IComponent介面(或派生自Component類)。

既然元件能夠請求窗體設計器的服務,那麼我們在編寫元件時,怎樣去取得這些服務呢?又怎樣去使用這些服務呢?注意在一般開發中,我們並不需要在元件中編寫與窗體設計器互動的程式碼,原因有兩個:

(1)這些事情是由元件開發者來做的,而我們大多數人只是充當元件的使用者;

(2)與窗體設計器互動的程式碼一般非常複雜,建議沒有特殊需要,不要在元件中編寫與窗體設計器(元件所在容器)互動的程式碼,因為這會影響IDE的穩定性。

下面示例程式碼演示了在元件中怎樣請求窗體設計器的服務:

 1 //Code 7-7
 2 //…include other namespace
 3 using System.ComponentModel;
 4 using System.ComponentModel.Design;
 5 class MyLabel:Label
 6 {
 7     //
 8     protected override void OnHandleCreated(EventArgs e)
 9    {
10           ISelectionService iss = GetService(typeof(ISelectionService)) as ISelectionService;
11           if(iss != null) //NO.1
12           {
13                  iss.SelectionChanged += new EventHandler(iss_SelectionChanged); //NO.2
14           }
15            base.OnHandleCreated(e);
16     }
17     void iss_SelectionChanged(object sender, EventArgs e)
18    {
19           ISelectionService iss = GetService(typeof(ISelectionService)) as ISelectionService;
20           if(iss != null) //NO.3
21           {
22                  Text = "窗體設計器中當前選中了" + iss.SelectionCount + "個元件\n"+ "第一個元件是" + (iss.PrimarySelection as Component).Site.Name; //NO.4
23          }
24     }
25 }

如上程式碼Code 7-7所示,在Label的派生類MyLabel中,我們重寫了OnHandleCreated虛方法,在該虛方法中向窗體設計器(容器)請求"元件選擇有關"的服務,窗體設計器返回一個ISelectionService的服務介面。注意我們需要先判斷返回來的服務介面是否為null(NO.1處),如果為null,說明當前元件不在任何容器中或者容器不提供該服務;如果不為null,我們使用該服務介面註冊窗體設計器的SelectionChanged事件(NO.2處),該事件會在窗體設計器選擇發生變化後被激發。之後在SelectionChanged的事件處理程式中,我們還要向窗體設計器請求服務,請求完成之後依舊要判斷返回來的服務介面是否為null(NO.3處),如果不為null,就使用返回來的ISelectionService介面將窗體設計器中選擇元件的個數顯示在Label.Text中(NO.4處)。示例程式碼最終的效果是:我們向窗體設計器拖放一個MyLabel控制元件,之後任何時候,只要窗體設計器中選擇元件發生了變化,MyLabel.Text屬性就會顯示當前選中元件的個數,而這一切都發生在窗體設計器中,也就是程式的開發階段,下圖7-9為效果圖:

圖7-9 元件與窗體設計器的互動

如上圖7-9所示,圖中窗體最上面的控制元件是一個MyLabel控制元件,我們在窗體設計器中選中了4個元件(注意窗體本身也算一個),MyLabel的Text屬性會根據窗體設計器中選擇元件的改變而變化。換句話說,元件與窗體設計器之間進行了互動,同理元件與元件之間透過窗體設計器提供的某些服務也可以進行互動。

注:Visual Studio中窗體設計器預設提供許多與設計有關的服務,服務介面大部分定義在System.ComponentModel.Design名稱空間中。只要我們知道服務規則,我們就可以在元件中請求這些服務,元件就可以與窗體設計器進行互動。容器永遠是服務規則的制定者,元件必須遵守這些規則方可正常工作。

前面說到過,程式開發過程中,原始碼才是我們最終所需要的東西。窗體設計器一關閉,設計器中所有的元件全部從記憶體中銷燬,我們用滑鼠鍵盤操作窗體設計器的時候,我們對設計器的任何一個操作,設計器都會幫助我們生成原始碼,我們透過屬性窗體編輯一個元件的屬性,元件能馬上產生效果的同時(比如背景色),窗體設計器也能為該操作生成原始碼。在Winform中,這些程式碼集中在InitializeComponent方法中。只要我們有了原始碼檔案,窗體設計器中的"假象"就可以隨時消失,窗體設計器中的元件和原始碼中的元件不是同一個東西,下圖7-10顯示了窗體設計器中的元件和設計器為我們生成的程式碼:

圖7-10 窗體設計器中的元件與生成的原始碼

如上圖7-10所示,圖中左邊顯示我們拖放到設計器中的一個Button控制元件,在這個過程中,窗體設計器除了會例項化一個Button控制元件(圖中左邊Form2中),還會為我們生成圖中右邊的程式碼。我們看到,生成的程式碼會例項化一個Button物件,然後將其加入到Form2.Controls的子控制元件集合中。圖中左邊設計器中的button1例項物件與圖中右邊生成程式碼中的button1變數不是同一個東西。

注意:元件不是一定要存在於容器中,它可以和其它物件一樣,單獨存在。換句話說,元件可以不存在於窗體設計器中,比如程式執行之後,程式中任何元件都不屬於窗體設計器,這將是下一節要討論的話題。

7.3 設計時(Design-Time)與執行時(Run-Time)

7.3.1 元件的設計時與執行時

7.2節中講窗體設計器時已經說過,窗體設計器中的各個元件都是真實存在的例項物件。我們向窗體設計器中拖放一個Button控制元件時,必然會呼叫Button類的構造方法例項化一個Button物件,Button物件必然會激發它的HandleCreated事件等等,也就是說,無論元件在哪裡,它都是以物件的形式真實存在的。

我們把元件處於窗體設計器中的狀態稱為"設計時",把元件正常執行時的狀態稱為"執行時"。設計時的元件和執行時的元件都是以物件的形式存在,因此有很多的相同點,比如都會呼叫構造方法,都會激發相應事件等等。除了這些相同點以外,還有一些不同點,由於處於設計時的元件存在容器中,因此它可以獲取窗體設計器中提供的服務,可以執行一些與窗體設計器互動的程式碼,而處於執行時狀態的元件不存在窗體設計器中,因此它不可以執行與窗體設計器互動有關的程式碼,見下圖7-11:

 

圖7-11 設計時元件與執行時元件

如上圖7-11所示,圓形代表元件,矩形代表窗體。圖中左邊顯示處於設計器中的元件,這些元件可以與窗體設計器互動,圖中右邊顯示處於執行狀態中的元件,它們不能再執行與窗體設計器互動有關的程式碼,因為它們根本就不在窗體設計器之中。

7.3.2 區分元件的當前狀態

任何元件都有兩種狀態:設計時和執行時。由於在設計時能執行的程式碼,在執行時可能執行失敗,相反,有些程式碼可能只需要在執行時執行,比如連線資料庫的程式碼,我們在設計時完全沒必要讓窗體設計器中的一個元件去連線資料庫。因此,我們編寫元件程式碼之前,一定要先搞清楚這些程式碼是在什麼狀態下去執行。我們可以先檢查一下元件的狀態,如果元件處於設計時,那麼執行程式碼A,否則不執行程式碼A。有兩種方式去判斷元件的當前狀態:

(1)元件的DesignMode屬性。每個元件都有一個Bool型別的DesignMode屬性,正如它的字面意思,如果該屬性為true,那麼代表元件當前處於設計時狀態;否則元件處於執行時狀態。我們將任何元件拖進窗體設計器後,設計器就會將元件的DesignMode設定為true(該屬性預設為false)。假如現在要開發一個控制元件,它具有顯示自己版本資訊的功能,但是僅僅在開發階段顯示,用於提醒開發者當前所用元件的版本,而當整個程式執行之後,版本資訊不再顯示,那麼該控制元件程式碼可以這樣寫:

 1 //Code 7-8
 2 class MyControl:Control
 3 {
 4     public MyControl()
 5     {
 6         //
 7     }
 8     protected override void OnPaint(PaintEventArgs e)
 9     {
10         e.Graphics.DrawRectangle(Pens.Blue,new Rectangle(0,0,Width-2,Height-2)); //NO.1
11         //
12         if(DesignMode) //NO.2
13         {
14             string v = "v2.30.109.1302"; //read from somewhere
15             using(Font f = new Font("arial",10))
16                 e.Graphics.DrawString(v,f,Brushes.Blue,new PointF(5,5));
17         }
18     }
19 }

如上程式碼Code 7-8所示,在MyControl控制元件的OnPaint方法中,我們先繪製了一個藍色邊框(NO.1),該行程式碼無論元件處在什麼狀態都會執行,因此我們可以看到拖到窗體設計器中的MyControl控制元件有一個藍色邊框,程式執行之後,窗體中的MyControl控制元件也有一個藍色邊框。接下來需要顯示版本資訊,因為只有當元件處於設計時狀態,才會顯示版本資訊,因此我們需要先判斷元件的當前狀態(NO.2處),如果DesignMode屬性為true,那麼說明元件處在窗體設計器中,需要顯示版本資訊;否則,說明元件處在執行時狀態,不顯示版本資訊。任何一個使用MyControl控制元件的開發者,都會在窗體設計器中看到當前MyControl控制元件的版本資訊,而當程式執行後,該版本資訊不再顯示。

(2)隨便請求一個服務,看返回來的服務介面是否為null。前面提到過,當一個元件不屬於任何一個容器時,那麼它透過GetService方法請求的服務肯定返回為null。因此,我們可以請求一個窗體設計器能夠提供的服務(比如前面用到過的ISelectService服務),看請求的返回值是否為null,如果為null,說明當前元件處於執行時;否則,當前元件處於窗體設計器中。前面的MyControl示例程式碼可以改為:

 1 //Code 7-9
 2 class MyControl:Control
 3 {
 4     public MyControl()
 5     {
 6         //
 7     }
 8     protected override void OnPaint(PaintEventArgs e)
 9     {
10         e.Graphics.DrawRectangle(Pens.Blue,new Rectangle(0,0,Width-2,Height-2)); //NO.1
11         //
12         ISelectionService iss = GetService(typeof(ISelectionService)) as ISelectionService;
13         if(iss != null) //NO.2
14         {
15             string v = "v2.30.109.1302"; //read from somewhere
16             using(Font f = new Font("arial",10))
17                 e.Graphics.DrawString(v,f,Brushes.Blue,new PointF(5,5));
18         }
19     }
20 }

如上程式碼Code 7-9所示,我們先請求一個ISelectionService服務,再判斷它的返回值是否為null(NO.2處),如果不為null,說明元件當前處於窗體設計器中;否則,元件處於執行時狀態。

注:(1)(2)方法均不適合巢狀元件,因為窗體設計器只會將最外層元件的DesignMode屬性值設定為true,如果這個元件內部還包含其它子元件,那麼這些子元件的DesignMode屬性還是原來的預設值false,因此(1)對巢狀元件中的子元件無效。我們拖放一個巢狀元件到窗體設計器中,只有最外層元件加入到了窗體設計器中,所以只有最外層元件能夠透過GetService方法請求窗體設計器的服務,內部的子元件由於沒有加入到容器,因此GetSeivice方法返回null,因此(2)對巢狀元件中的子元件也無效。有一種可以解決巢狀元件中無法判斷其子元件狀態的方法,那就是透過Process類來檢查當前程式的名稱,看是否包含"devenv"這個字串,如果是,那麼說明元件當前處於Visual Studio開發環境中(即元件處於設計時),if(Process.GetCurrentProcess().ProcessName.Contains("devenv"))為假,說明元件處於執行時。這種方法也有一個弊端,很明顯,如果我們使用的不是Visual Studio開發環境(也就是程式名不包含devenv),或者我們自己的程式程式名稱就包含devenv怎麼辦呢?

7.3.3 元件狀態的應用

作為一名普通的開發人員,幾乎不需要接觸到元件狀態這些概念,大部分開發人員只是使用元件,也就只需要編寫好元件在執行時需要執行的程式碼即可,如果你是一個元件開發人員,那麼你就可能需要與窗體設計器打交道,控制元件與設計器之間的互動,能讓元件的使用者更加方便的去使用元件。

在開發一些需要授權的元件時,就可以用到元件的兩種狀態,這些需要授權的元件收費物件一般是開發者,因此,在開發者使用這些元件開發系統的時候(處於開發階段),就應該有授權入口,而當程式執行之後,就不應該出現授權的介面,這時候就可以根據元件的當前狀態來判斷是否需要顯示授權入口。

如果我們編寫了與窗體設計器互動的程式碼,那麼一定要謹慎小心,因為訪問窗體設計器的程式碼很容易就會造成開發環境崩潰。

7.4 控制元件

7.4.1 控制元件基類

本章7.1.3小節中已經介紹了Windows Forms中的控制元件分類及其派生關係。控制元件作為元件中的一個分支,具有視覺化顯示的功能,言下之意就是控制元件內部具備Windows訊息處理的功能(詳見第八章)。System.Windows.Forms.Control類是所有控制元件的基類,它內部已經提供了所有控制元件必須具備的基礎結構,比如視窗控制程式碼、視窗過程以及基礎的Windows訊息路由。

Control類的預設外觀顯示為一個矩形,Windows Forms框架中其它所有控制元件均派生自Control類。

7.4.2 使用者自定義控制元件

Windows Forms中包含有非常強大也非常完善的控制元件,比如基礎控制元件Button、CheckBox等,容器佈局控制元件Panel、TabControl等,選單控制元件MenuStrip以及資料顯示控制元件DataGridView控制元件等。這些控制元件在一般開發中可以滿足我們的需要,但是有些時候對於一些特殊的功能需求,使用系統自帶的控制元件遠遠不夠,因此,我們需要自己開發滿足功能要求的控制元件。有三種方式開發新的控制元件:

(1)複合控制元件(Composite Control);

這種方式很簡單,就是將已有控制元件組合在一起,形成一個整體,將現有控制元件功能集中起來。我們平時開發的"使用者控制元件",從UserControl類派生而來,將許許多多現有控制元件集中到一起,這種控制元件就屬於複合控制元件,見下面程式碼:

1 //Code 7-10
2 class MyUserControl:UserControl
3 {
4     public MyUserControl()
5     {
6         InitializeComponent(); //NO.1
7     }
8     //
9 }

如上程式碼Code 7-10所示,在MyUserControl類中的InitializeComponent方法中(NO.1處),我們可以將現有的控制元件組合在一起,形成一個整體。注意InitializeComponent方法中的程式碼一般由窗體設計器生成,我們每次透過窗體設計器向MyUserControl中新增一個控制元件,在InitializeComponent方法中都會生成類似"this.Controls.Add(…)"這樣的程式碼,意思就是將新新增的控制元件加入MyUserControl.Controls集合中。

(2)擴充套件控制元件(Extended Control);

從現有控制元件派生出一個新控制元件。如果現有控制元件基本已經滿足需求,只是需要稍微增加一些小功能或者稍微修改現有功能,那麼我們可以將已有控制元件作為基類,派生出一個新控制元件,在派生類中編寫增加功能或者修改功能的程式碼,下面程式碼演示了一個從Button類派生的MyButton類,改變了原來Button的顯示外觀,在原來顯示外觀的基礎上繪製了一個藍色矩形:

1 //Code 7-11
2 class MyButton : Button
3 {
4     protected override void OnPaint(PaintEventArgs pevent)
5     {
6         base.OnPaint(pevent);
7         pevent.Graphics.DrawRectangle(Pens.Blue, new Rectangle(1, 1, Width - 3, Height - 3)); //NO.1
8     }
9 }

如上程式碼Code 7-11所示,我們在MyButton類中重寫了OnPaint虛方法,每次控制元件需要重繪的時候,我們在控制元件介面繪製一個藍色矩形(NO.1處),除了顯示外觀的差別,MyButton類與Button類具有完全一樣的功能。本示例程式碼只是在Button類的基礎上進行一個非常簡單的修改。

(3)自定義控制元件(Custom Control)。

以Control為基類,直接派生出一個新控制元件。這種方式對開發者的技術能力要求較高,因為Control類中只是包含了所有控制元件應該具備的基礎結構,它並不提供控制元件的特定功能以及顯示介面,因此無論從功能的實現還是介面的顯示均要求開發者自己去處理,比如控制元件的外觀顯示,開發者必須熟悉GDI和重寫OnPaint虛方法,而對於控制元件的一些功能實現,開發者必須熟悉Windows訊息和重寫控制元件的視窗過程WndProc這個虛方法等。正是因為這種從底層都需要開發者自己去實現的做法,才讓我們更靈活地控制控制元件的行為和外觀,下面示例程式碼演示如何從Control類派生出新控制元件:

 1 //Code 7-12
 2 class MyControl:Control
 3 {
 4     public event EventHandler Event1; //NO.1
 5     public event EventHandler Event2; //NO.2
 6     public MyControl()
 7     {
 8         //
 9     }
10     protected override void OnPaint(PaintEventArgs e)
11     {
12         base.OnPaint(e);
13         // paint the surface of MyControl
14         e.Graphics.DrawRectangle(Pens.Blue, new Rectangle(1, 1, Width - 3, Height - 3)); //NO.3
15     }
16     protected override void WndProc(ref Message m)
17     {
18         if(m.Msg == ?) //NO.4
19         {
20             //raise Event1 with Message's arguments
21             return;
22         }
23         if(m.Msg == ?) //NO.5
24         {
25             //raise Event2 with Message's arguments
26             return;
27         }
28         //
29         base.WndProc(ref m);
30     }
31 }

如上程式碼Code 7-12所示,我們在MyControl類中重寫了OnPaint虛方法,負責繪製控制元件的外觀顯示(NO.3處),還重寫了WndProc虛方法,攔截Windows訊息(NO.4和NO.5處),將訊息引數轉換成事件引數,最後激發相應事件(這個過程參考第八章),注意重寫的虛方法中不要忘記呼叫基類的虛方法。

注:無論是複合控制元件、擴充套件控制元件還是自定義控制元件,我們均可以重寫控制元件的視窗過程:WndProc虛方法,從根源上接觸到Windows訊息,這個做法並不是自定義控制元件的專利。

7.5 本章回顧

本章講到的"元件"是指.NET程式設計中的元件,特指直接或間接實現了IComponent介面的型別,它跟我們通常意義上談到的元件有很大的區別。元件在.NET程式設計中起到了重要作用,所有可在IDE中視覺化設計的型別必須是元件,包括我們直接從工具箱中拖到窗體設計器中的UI相關元件(如Button按鈕)和其它功能元件(如backgroundWorker)。本章還介紹了"容器-元件-服務"模型,介紹了窗體設計器與元件之間的關係,以及為什麼一個元件可以放在設計器中進行視覺化設計,之後還介紹了元件的兩種狀態:設計時(Design-Time)和執行時(Run-Time),元件的這兩種狀態對元件的行為表現起到了重要作用。

7.6 本章思考

1..NET程式設計程式碼中元件的定義是?

A:特指實現(直接或間接)了System.ComponentModel.IComponent介面的型別,只有元件才可以在窗體設計器中進行視覺化設計。

2."容器-元件-服務"模型中容器的含義是?

A:特指實現(直接或間接)了System.ComponentModel.IContainer介面的型別,不包括ArrayList、Queue、Stack以及Array等物理容器。

3.元件有哪兩種狀態?怎樣區分元件的當前狀態?

A:元件有"設計時(Design-Time)"與"執行時(Run-Time)"兩種狀態,可以透過元件的DesignMode屬性去判斷它的當前狀態,一般情況下,如果該屬性為true,說明當前元件處於設計時,否則處於執行時(參見本章7.3.2小節)。在窗體設計器中建立的元件的狀態即為設計時,程式執行後元件的狀態即為執行時。

相關文章