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

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

操作 加載 痛苦 int 部分 容器服務 tcollect 特征 主窗體

本文討論:

?

設計時環境基本原理

?

窗體設計器體系結構

?

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)— 這可能是一種非常有用的調試手段,值得添加到您的方法庫中。

小結

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

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