1. 程式人生 > >通過用 .NET 生成自定義窗體設計器來定製應用程式

通過用 .NET 生成自定義窗體設計器來定製應用程式

本文討論:

?

設計時環境基本原理

?

窗體設計器體系結構

?

Visual Studio .NET 中窗體設計器的實現

?

為自己的應用程式編寫窗體設計器而需要實現的服務

在很多年中,MFC 一直是生成基於 Windows? 的應用程式的流行框架。MFC 包含一個可以使窗體生成、事件連通和其他基於窗體的程式設計任務更加容易的窗體設計器。儘管 MFC 被廣泛使用,但它一直由於其自身的缺點而受到批評 — 這些缺點大多存在於 Microsoft? .NET Framework 所擅長的領域。事實上,.NET Framework 中 Windows 窗體的可擴充套件及可插拔設計時體系結構已經使開發工作變得比用 MFC 進行開發靈活得多。

例如,通過 Windows 窗體,可以將某個自定義控制元件從工具箱拖放到 Visual Studio? 設計圖面上。令人驚訝的是,即使 Windows 窗體不瞭解有關該控制元件的任何資訊,它也能夠承載它並讓您操縱它的屬性。這些在 MFC 中都是不可能的。

在本文中,我將討論在設計窗體時發生在幕後的事情。然後,我將向您說明如何生成自己的基本窗體設計器,以使使用者能夠按照與使用 Visual Studio 中的窗體設計器建立窗體的類似方式來建立窗體。為了完成這一工作,您需要確切瞭解 .NET Framework 提供了哪些功能。

一些 Windows 窗體基礎知識

在我開始進行該專案之前,您需要先了解幾個基本概念。讓我們從設計器的定義開始。設計器提供了元件的設計模式 UI 和行為。例如,在窗體上放置按鈕時,按鈕的設計器就是確定該按鈕的外觀和行為的實體。設計時環境提供了一個窗體設計器和一個屬性編輯器,以使您可以操縱元件和生成使用者介面。設計時環境還提供了可用來與設計時支援進行互動以及自定義和擴充套件設計時支援的服務。

窗體設計器提供了設計時服務和一個供開發人員設計窗體的工具。設計器宿主使用設計時環境來管理設計器狀態、活動(例如,事務)和元件。此外,還有幾個需要了解的與元件本身有關的概念。例如,元件是可處置的,它可以由容器託管,並提供了 Site 屬性。它通過實現 IComponent 而獲得這些特徵,如下所示:

public interface System.ComponentModel.IComponent : IDisposable  {      ISite Site { get; set; }      public event EventHandler Disposed;  }  

IComponent 介面是設計時環境和要在設計圖面(例如,Visual Studio 窗體設計器)上承載的元素之間的基本協定。例如,按鈕可以寄宿到 Windows 窗體設計器中,因為它實現了 IComponent。

.NET Framework 實現兩個型別的元件:可視元件和非可視元件。可視元件是使用者介面元素(例如,控制元件),而非可視元件是沒有使用者介面的元件(例如,建立 SQL Server? 資料庫連線的元件)。Visual Studio .NET 窗體設計器在您將元件拖放到設計圖面上時,對可視元件和非可視元件加以區分。圖 1 顯示了這一區別的一個示例。

圖 1 可視元件和非可視元件

容器包含元件,並且允許所包含的元件相互訪問。當容器管理元件時,該容器負責在自身被處置時處置該元件 — 這是一個好主意,因為元件可以使用非託管資源,而這些資源不會由垃圾回收器自動處理。容器實現了 IContainer,IContainer 只是幾個使您可以在該容器中新增和移除元件的方法:

public interface IContainer : IDisposable  {         ComponentCollection Components { get; }      void Add(IComponent component);      void Add(IComponent component, string name);      void Remove(IComponent component);  }  

不要讓該介面的簡單性欺騙了您。容器的概念在設計時很關鍵,並且在其他情況下也很有用。例如,您肯定編寫過例項化多個可處置元件的業務邏輯。它通常採用下面的形式:

using(MyComponent a = new MyComponent())  {      // a.do();  }  using(MyComponent b = new MyComponent())  {      // b.do();  }  using(MyComponent c = new MyComponent())  {      // c.do();  }  

使用 Container 物件,可以將上述程式碼行簡化為下面的形式:

using(Container cont = new Container())  {      MyComponent a = new MyComponent(cont);      MyComponent b = new MyComponent(cont);       MyComponent c = new MyComponent(cont);      // a.do();      // b.do();       // c.do();  }  

容器的功能不只限於自動處置它的元件。.NET Framework 定義了一個名為“站點”的東西,它與容器和元件相關。這三者之間的關係如圖 2 所示。正如您可以看到的那樣,元件剛好由一個容器管理,並且每個元件剛好具有一個站點。在生成窗體設計器時,同一個元件不能出現在一個以上的設計圖面上。但是,多個元件可以與同一個容器相關聯。

 

圖 2 關係

元件的生存期可以由它的容器來控制。作為生存期管理的回報,元件獲得了對容器所提供的服務的訪問權。此關係類似於 COM+ 元件與承載它的 COM+ 容器之間的關係。通過允許 COM+ 容器對其進行管理,COM+ 元件可以參與事務以及使用由 COM+ 容器提供的其他服務。在設計時上下文中,元件和它的容器之間的關係是通過站點建立的。在將元件放到窗體中時,設計器宿主會為該元件和它的容器建立一個站點例項。當此關係建立以後,元件已經被“站點化”,並使用它的 ISite 屬性來訪問它的容器所提供的服務

服務和容器

當元件允許容器取得它的所有權時,該元件就獲得了對該容器所提供的服務的訪問權。在該上下文中,服務可以被視為具有眾所周知的介面的函式,可以從服務提供程式中獲得,可以儲存在服務容器中,並且可以通過它的型別定址。

服務提供程式實現了 IServiceProvider,如下所示:

public interface IServiceProvider   {      object GetService(Type serviceType);  }  

客戶端通過向服務提供程式的 GetService 方法提供它們所需的服務型別來獲得服務。服務容器充當服務的儲存庫並實現 IServiceContainer,從而提供了一種新增和移除服務的手段。下面的程式碼顯示了 IServiceContainer 的定義。請注意,服務定義只包含用於新增和移除服務的方法。

public interface IServiceContainer : IServiceProvider  {      void AddService(Type serviceType,ServiceCreatorCallback callback);      void AddService(Type serviceType,ServiceCreatorCallback callback,                       bool promote);      void AddService(Type serviceType, object serviceInstance);      void AddService(Type serviceType, object serviceInstance,                       bool promote);      void RemoveService(Type serviceType);      void RemoveService(Type serviceType, bool promote);  }  

因為服務容器可以儲存和檢索服務,所以它們還被視為服務提供程式,並因此實現了 IServiceProvider。服務的組合、服務提供程式和服務容器共同構成了一個具有很多優點的簡單設計模式。例如,該模式具有下列優點:

?

在客戶端元件和它們所使用的服務之間建立了鬆耦合。

?

建立了簡單的服務儲存庫和發現機制,從而使應用程式(或應用程式的某些部分)能夠良好地伸縮。您可以只使用必要的部分生成應用程式,然後再新增其他服務,而無需對應用程式或模組進行任何較大的更改。

?

提供了用於實現服務的惰性載入的工具。AddService 方法被過載,以便在第一次查詢服務時建立相應的服務。

?

可以用作靜態類的替代品。

?

促進了基於協定的程式設計。

?

可以用來實現工廠服務。

?

可以用來實現可插拔的體系結構。您可以使用這種簡單的模式來載入外掛,以及向外掛提供服務(例如,日誌記錄和配置)。

設計時基礎結構極為廣泛地使用了該模式,因此徹底理解它是很重要的。

生成窗體設計器

既然您已經瞭解了設計時環境背後的基本概念,那麼我將以它們為基礎來分析窗體設計器的體系結構(請參見圖 3)。

 

圖 3 窗體設計器體系結構

體系結構的核心是元件。所有其他實體都直接或間接地使用元件。窗體設計器是將其他實體連線在一起的粘接劑。窗體設計器使用設計器宿主來獲得對設計時基礎結構的訪問權。設計器宿主使用設計時服務,並且提供它自己的一些服務。服務可以(並且通常)使用其他服務。

.NET Framework 沒有公開 Visual Studio .NET 中的窗體設計器,因為該實現是特定於應用程式的。儘管實際的介面未公開,但設計時框架仍然存在。您必須完成的所有工作就是提供特定於窗體設計器的實現,然後將您的版本提交給設計時環境以供使用。

我的示例窗體設計器顯示在圖 4 中。像每個窗體設計器一樣,它具有一個供使用者選擇工具或控制元件的工具箱、一個用於生成窗體的設計圖面以及一個用於操縱元件屬性的屬性網格。

圖 4 自定義窗體設計器示例

首先,我將生成工具箱。但是,在此之前,我需要決定如何向用戶呈現工具。Visual Studio .NET 具有一個包含多個組的導航欄,其中的每個組都包含有工具。要生成工具箱,您必須完成下列工作:

1.

建立向用戶顯示工具的使用者介面

2.

實現 IToolboxService

3.

將 IToolboxService 實現插入到設計時環境中

4.

處理事件,例如,工具的選擇和拖放

對於任何現實的應用程式,生成工具箱使用者介面可能很費時間。您必須做出的第一個設計決策是如何發現和載入工具 — 有多種可行的方法。使用第一個方法,可以對要顯示的工具進行硬編碼。建議不要採用這種方法,除非應用程式非常簡單,並且將來只需要進行很少的維護。

第二個方法涉及到從配置檔案中讀取工具。例如,工具可以按如下方式定義:

<Toolbox>      <ToolboxItems>          <ToolboxItem DisplayName="Label"              Image="ResourceAssembly,Resources.LabelImage.gif"/>          <ToolboxItem DisplayName="Button"              Image="ResourceAssembly,Resources.ButtonImage.gif"/>          <ToolboxItem DisplayName="Textbox"              Image="ResourceAssembly,Resources.TextboxImage.gif"/>      </ToolboxItems>  </Toolbox>       

該方法的優點是可以新增或削減工具,而無需重新編譯程式碼以改變工具箱中顯示的工具。另外,該實現相當簡單。您需要實現一個節處理程式來讀取 Toolbox 節,並返回 ToolboxItem 的列表。

第三個方法是為每個工具建立一個類,並用封裝了諸如顯示名稱、組和點陣圖之類資訊的特性來修飾該類。在啟動時,應用程式會載入一組程式集(從配置檔案或類似東西中指定的某個眾所周知的位置),然後查詢帶有特定修飾(例如,ToolboxAttribute)的型別。具有該修飾的型別被載入到工具箱中。該方法可能是最靈活的方法,並且可以通過反射來進行了不起的工具發現,但是它也需要完成更多一些工作。在我的示例應用程式中,我使用了第二個方法。

下一個重要步驟是獲得工具箱影象。您可以花費好幾天來嘗試建立自己的工具箱影象,但是以某種方式訪問 Visual Studio .NET 工具箱中的工具箱影象將非常方便。幸運的是,已經有了完成該工作的方法。在內部,Visual Studio .NET 工具箱是使用第三個方法的變體載入的。這意味著,元件和控制元件是用一個特性 (ToolboxBitmapAttribute) 修飾的,該特性定義了為該元件或控制元件獲得影象的位置。

在示例應用程式中,工具箱內容(組和項)在應用程式配置檔案中定義。為了載入工具箱,一個自定義的節處理程式會讀取 Toolbox 節,並返回一個繫結類。該繫結類隨後被傳遞給表示該工具箱的 TreeView 控制元件的 LoadToolbox 方法,如圖 5 所示。

LoadItem 方法為給定型別建立一個 ToolboxItem 例項,然後呼叫 GetItemImage 來獲得與該型別相關聯的影象。該方法獲得該型別的特性集合以查詢 ToolboxBitmapAttribute。如果它找到該特性,則會返回影象,以便它可以與剛剛建立的 ToolboxItem 相關聯。請注意,該方法使用 TypeDescriptor 類,此類是 System.ComponentModel 名稱空間中的一個實用性的類,它用於獲得給定型別的特性和事件資訊。

既然您知道了如何生成工具箱使用者介面,那麼下一個步驟是實現 IToolboxService。由於該介面被直接繫結到工具箱,所以在派生自 TreeView 的類中實現該介面十分方便。大多數實現都很簡單明瞭,但是您需要特別注意如何處理拖放操作,以及如何序列化工具箱項。請參見本文程式碼下載(可從 MSDN?Magazine Web 站點獲得)中的 ToolboxService 實現的 toolboxView_MouseDown 方法。該過程的最後一步是將服務實現掛鉤到設計時環境中 — 在討論完如何實現設計器宿主之後,我將演示如何進行掛鉤。

實現服務

窗體設計器基礎結構是在服務之上生成的。有一組服務必須實現,還有一些服務只是增強窗體設計器的功能(如果您實現它們的話)。這是我在前面討論的服務模式以及窗體設計器的一個重要方面。您可以首先實現基本服務集,以後再新增其他服務。

設計器宿主是到設計時環境的掛鉤。設計時環境使用宿主服務在使用者從工具箱中拖放元件時建立新元件,管理設計器事務,在使用者操縱元件時查詢服務,等等。宿主服務定義 IDesignerHost 定義了方法和事件。在宿主實現中,您需要為宿主服務以及其他多個服務提供實現。這些服務應當包括 IContainer、IComponentChangeService、IExtenderProviderService、ITypeDescriptionFilterService 和 IDesignerEventService。

設計器宿主

設計器宿主是窗體設計器的核心。當宿主的建構函式被呼叫時,該宿主使用父服務提供程式 (IServiceProvider) 來構建它的服務容器。以這種方式將提供程式串連起來以達到涓流效果是很常見的。在建立了服務容器之後,宿主將它自己的服務新增到提供程式中,如圖 6 所示。

將元件放到設計圖面上時,需要將其新增到宿主的容器中。新增新元件是一項相當複雜的操作,因為必須執行多項檢查,並且還要激發一些事件(請參見圖 7)。

如果忽略檢查和事件,則可以按如下方式總結新增演算法。首先,為該型別建立一個新的 IComponent,並且為該元件建立一個新的 ISite。這會建立站點-元件關聯。請注意,站點的建構函式接受設計器宿主例項。站點建構函式採用設計器宿主和元件,以便可以建立圖 2 中所示的元件-容器關係。然後,建立、初始化該元件設計器,並將其新增到元件-設計器詞典中。最後,將新元件新增到設計器宿主容器中。

移除元件需要完成一點兒清理工作。同樣,如果忽略簡單檢查和驗證,則移除操作實際上就是移除設計器,處置設計器,移除該元件的站點,然後處置該元件。

設計器事務

設計器事務的概念類似於資料庫事務,因為它們都是將一系列操作組合在一起,以便將該組操作視為一個工作單元,並啟用提交/中止機制。設計器事務在整個設計時基礎結構中使用,以便支援操作的取消,並且使檢視能夠延遲更新它們的顯示,直到整個事務完成為止。設計器宿主提供了通過 IDesignerHost 介面來管理設計器事務的工具。管理事務並不非常困難(請參見示例應用程式中的 DesignerTransactionImpl.cs)。

DesignerTransactionImpl 表示事務中的單個操作。當宿主被要求建立事務時,它會建立 DesignerTransactionImpl 的一個例項來管理單個更改。該宿主在 DesignerTransactionImpl 的例項管理每個更改的同時跟蹤事務。如果您沒有實現事務管理,則會在使用窗體設計器時獲得一些有趣的異常。

介面

正如我已經說過的那樣,需要將元件放到容器中,才能進行生存期管理以及向它們提供服務。設計器宿主介面 IDesignerHost 定義了用於建立和移除元件的方法,因此如果宿主提供了該服務,您不應當感到吃驚。同樣,容器服務定義了用於新增和移除元件的方法,這些方法與 IDesignerHost 的 CreateComponent 和 DestroyComponent 方法重疊。因此,大多數繁重工作都是在容器的新增和移除方法中完成的,而建立和銷燬方法只是將呼叫轉發給這些方法。

IComponentChangeService 定義了元件更改、新增、移除和重新命名事件。它還為元件的已更改事件和正在更改的事件定義了方法,當元件正在更改或已經更改時(例如,當屬性更改時),這些方法由設計時環境呼叫。該服務由設計器宿主提供,這是因為元件是通過宿主建立和銷燬的。除了建立和銷燬元件以外,宿主還可以通過建立方法來處理元件重新命名操作。重新命名邏輯很簡單,但很有趣:

// If I own the component and the name has changed, rename the component  if (component.Site != null && component.Site.Container == this &&        name != null && string.Compare(name,component.Site.Name,true) != 0)   {      // name validation and component changing/changed events are       // fired in the Site.Name property so I don't have       // to do it here...      component.Site.Name=name;      return;  }  

該介面的實現足夠簡單,您完全可以將其餘部分留待示例應用程式予以解決。

ISelectionService 處理設計圖面上的元件選擇。當用戶選擇元件時,SetSelectedComponents 方法由帶有所選元件的設計時環境呼叫。SetSelectedComponents 的實現顯示在圖 8 中。

選擇服務會跟蹤設計器表面上的元件選擇。其他服務(例如,IMenuCommandService)在需要獲得有關所選元件的資訊時使用該服務。為了提供此資訊,該服務將維護一個表示當前所選元件的內部列表。設計時環境在元件的選擇已經被更改時用一個元件集合來呼叫 SetSelectedComponents。例如,如果使用者選擇了一個元件,然後按住 shift 鍵並選擇另外三個元件,則每次向選擇列表中進行新增時,都會呼叫該方法。每次呼叫該方法時,設計時環境都會告訴我們哪些元件受到了影響,以及受到了怎樣的影響(通過 SelectionTypes 列舉)。實現會檢視元件是如何更改的,以便確定元件是需要新增到內部選擇列表中,還是需要從該列表中移除。在修改內部選擇列表以後,我激發了 Selection Changed 事件(請參見 SelectionServiceImpl.cs 中的方法 selectionService_SelectionChanged),以便可以用新的選擇更新屬性網格。應用程式的主窗體 MainWindow 預訂了選擇服務的 Selection Changed 事件,以便用所選的元件更新屬性網格。

另請注意,選擇服務定義了 PrimarySelection 屬性。主選擇始終設定為所選的最後一個項。當我討論如何顯示正確的設計器上下文選單時,我將在 IMenuCommandService 的討論中使用該屬性。

選擇服務是比較難以正確實現的服務之一,因為它具有一些使實現複雜化的有價值的功能。例如,在現實的應用程式中,處理鍵盤事件(例如,Ctrl+A)以及管理與處理大型選擇列表有關的問題是有意義的。

ISite 實現是比較重要的實現之一,如圖 9 所示。

您將注意到 SiteImpl 還實現了 IDictionaryService,這有一點兒不同尋常,因為我實現的所有其他服務都繫結到設計器宿主。結果,設計時環境要求您為每個站點化元件實現 IDictionaryService。設計時環境使用每個站點上的 IDictionaryService 來維護在整個設計器框架中使用的資料表。另一個需要注意的與站點實現有關的事情是,由於 ISite 擴充套件了 IServiceProvider,因此類提供了 GetService 的實現。設計器框架在站點上查詢服務實現時呼叫該方法。如果服務請求是針對 IDictionaryService 的,則該實現只會返回自身 — SiteImpl。對於所有其他服務,請求被轉發給站點的容器(例如,宿主)。

每個元件都必須具有一個唯一的名稱。當您將元件從工具箱中拖放到設計圖面上時,設計時環境會使用 INameCreationService 的實現來生成每個元件的名稱。元件的名稱是在該元件被選擇時顯示在屬性視窗中的 Name 屬性。INameCreationService 介面的定義如下所示:

public interface INameCreationService   {      string CreateName(IContainer container, Type dataType);      bool IsValidName(string name);      void ValidateName(string name);  }  

在示例應用程式中,CreateName 實現使用容器和 dataType 來計算新名稱。簡言之,該方法統計其型別等價於 dataType 的元件的數量,然後將得到的計數與 dataType 結合使用來提出一個唯一的名稱。

迄今為止所討論的服務都直接或間接地處理元件。另一方面,選單命令服務是特定於設計器的。它負責跟蹤選單命令和設計器謂詞(操作),並且在使用者選擇特定設計器時顯示正確的上下文選單。

選單命令服務處理新增、移除、查詢和執行選單命令的任務。此外,它還定義了相關方法,以便跟蹤設計器謂詞,以及為支援這些方法的設計器顯示設計器上下文選單。該實現的核心在於顯示正確的上下文選單。因此,我將剩下的一點兒實現留到示例應用程式中,而重點討論如何顯示上下文選單。

跟蹤設計器謂詞並顯示上下文選單

有兩種型別的設計器謂詞:全域性謂詞和區域性謂詞。全域性謂詞適合於所有設計器,而區域性謂詞特定於每個設計器。當您在設計圖面上右鍵單擊選項卡控制元件時,可以看到一個區域性謂詞的示例(請參見圖 10)。

圖 10 設計圖面

右鍵單擊選項卡控制元件可以新增區域性謂詞,以使您可以在控制元件上新增和移除選項卡。當您在 Visual Studio 窗體設計器中右鍵單擊設計圖面的任何地方時,可以看到一個全域性謂詞的示例。無論您單擊哪個地方或哪個物件,您始終會看到以下兩個選單項:“View Code”和“Properties”。每個設計器都具有一個 Verbs 屬性,該屬性包含代表特定於該設計器的功能的謂詞。例如,對於選項卡控制元件設計器,謂詞集合包含以下兩個成員:“Add Tab”和“Remove Tab”。

當用戶右鍵單擊設計圖面上的選項卡控制元件時,設計時環境將呼叫 IMenuCommandService 上的 ShowContextMenu 方法(請參見圖 11)。

該方法負責顯示所選物件的設計器的上下文選單。正如您在圖 11 中看到的那樣,該方法從選擇服務中獲得所選元件,從宿主中獲得它的設計器,從設計器中獲得謂詞集合,然後向每個謂詞的上下文選單中新增一個選單項。在添加了謂詞之後,上下文選單將顯示。請注意,當您為設計器謂詞建立新的選單項時,您還為該選單項附加了一個單擊處理程式。該自定義單擊處理程式可為所有選單項處理單擊事件(請參見示例應用程式中的方法 MenuItemClickHandler)。

當用戶從設計器上下文選單中選擇選單項時,系統將呼叫該自定義處理程式,以執行與該選單項關聯的謂詞。在該處理程式中,可以檢索與該選單項相關聯的謂詞並呼叫它。

ITypeDescriptorFilterService

我在前面提到過,TypeDescriptor 類是一個實用性的類,它用於獲得有關型別的屬性、特性和事件的資訊。ITypeDescriptorFilterService 可以為站點化元件篩選該資訊。TypeDescriptor 類在試圖返回站點化元件的屬性、特性和/或事件時使用 ITypeDescriptorFilterService。如果設計器希望為它正在設計的元件修改設計時環境可用的元資料,則可以通過實現 IDesignerFilter 來完成該工作。ITypeDescriptorFilterService 定義了三個方法,以使設計器篩選器可以掛鉤到站點化元件的元資料中並對其進行修改。ITypeDescriptorFilterService 的實現簡單而直觀(請參見示例應用程式中的 TypeDescriptorFilterService.cs)。

把程式碼合在一起

如果您已經查看了示例應用程式並且執行窗體設計器,則您可能想知道所有這些服務是如何整合在一起的。您不能以遞增方式生成窗體設計器 — 也就是說,您不能實現一個服務,測試應用程式,然後編寫另一個服務。您必須實現所有必需的服務,生成使用者介面,將它們全都結合在一起,然後才能測試應用程式。這是壞訊息。好訊息是,我已經在我所實現的服務中完成了大部分工作。所剩下的只是一點兒技巧。

首先,請觀察一下設計器宿主的 CreateComponent 方法。在建立新元件時,需要了解它是否是第一個元件(如果 rootComponent 為空)。如果它是第一個元件,則您必須為該元件建立專門的設計器。這一專門的基礎設計器是一個 IRootDesigner,因為設計器層次結構中最頂層的設計器必須是 IRootDesigner(請參見圖 12)。

既然您知道了第一個元件必須是根元件,那麼如何確保正確的元件是第一個元件呢?答案是設計圖面最終成為第一個元件,因為您在主視窗初始化例程中將該控制元件建立為 Form(請參見圖 13)。

處理根元件是設計器宿主、設計時環境和使用者介面之間的粘合劑的唯一需要技巧的部分。其餘部分只需花費一點兒時間閱讀程式碼就很容易理解。

除錯專案

實現窗體設計器不是一個沒有價值的練習。幾乎沒有任何有關該主題的現存文件。在您弄清楚從哪裡開始以及要實現哪些服務之後,除錯專案將是一項令人痛苦的工作,因為您必須實現一組必需的服務並將它們插入到專案中,然後才能開始除錯任何服務。最後,在實現了必需的服務之後,所得到的錯誤資訊不會提供多大的幫助。例如,您可能在呼叫內部設計時程式集的行中得到 NullReferenceException,而您無法除錯該錯誤,因此您只能納悶哪個服務在哪個地方失敗了。

另外,因為設計時基礎結構是在我前面討論的服務模式之上生成的,所以除錯服務可能會成為一個問題。一種可以減輕除錯痛苦的技術是記錄服務請求。記錄哪個服務請求被查詢,該請求是通過還是失敗,以及它是從框架內部的哪個地方呼叫的(利用 Environment.StackTrace)— 這可能是一種非常有用的除錯手段,值得新增到您的方法庫中。

小結

我已經概述了為了使窗體設計器啟動和執行而需要實現的基礎服務。此外,您已經瞭解瞭如何通過更改配置檔案來基於應用程式的需要配置工具箱。剩下的工作就是調整現有的服務,並根據您的需要來實現其他一些服務。