利用 .NET Framework 2.0 建立並宿主自定義的設計
釋出日期: 2006-6-15 | 更新日期: 2006-6-15
本文討論:
|
本文使用下列技術: |
本頁內容
Microsoft® .NET 中實現,需要第三方程式碼來重寫所有這些複雜的邏輯。現在,這種情況已經有所改變;.NET Framework 2.0 引入了一組類,用於宿主現成的設計器。
圖 1 執行時
要了解 .NET Framework 設計器的工作方式,重要的是瞭解如何使用這些設計器。設計器是一個物件,它僅存在於設計時,並連線到執行時存在的物件。.Net Framework 連線這兩種物件,併為設計時物件提供一種渠道以擴大執行時物件的行為。在執行時,窗體及其上面的按鈕僅通過兩個控制元件間的父/子關係進行連線(參見圖 1)。不存在其他物件控制這些控制元件的生存期。
圖 2
該圖在設計時看起來比較複雜。窗體和按鈕均有與之相關的設計器。這兩個物件也連線到擁有這兩個物件的宿主容器(參見圖 2)。宿主容器還提供以下服務,例如選擇服務(在設計時選擇一個或多個物件)、顯示訊息的 UI 服務、呼叫幫助以及與開發環境進行互動 — 物件和設計器可以使用。
宿主容器還承擔許多職責。它建立元件,將它們繫結到設計器,併為其維護的元件和設計器提供服務。它從某種永續性狀態載入設計器,並將它們儲存回該狀態。宿主容器提供撤銷邏輯、剪貼簿功能、以及其他服務 — 設計器需要以其為基礎來提供一個健壯的設計時環境。
圖 3 設計器宿主
宿主容器還能夠持久保持設計器的狀態。為此,它需要使用一個設計器載入器。設計器載入器可以使用序列化程式依次對元件進行序列化,如圖 3 所示。
利用服務提高可擴充套件性
.NET Framework 設計器體系結構是可擴充套件的。可擴充套件性的關鍵在於,服務能夠增強各種設計器的可用功能。服務是一種物件,可根據型別進行查詢。通常,您定義一些代表服務的抽象類或介面,然後提供對該服務的實現。您可以將服務新增到呼叫服務容器的物件,也可以從該物件中刪除服務。IDesignerHost — 設計器的主要宿主介面,它是一個服務容器。服務是一種功能,可在由不同方編寫的元件之間進行共享。因為這個原因,您必須在使用和建立服務時沿襲某些規則。
服務無法獲得保證。無論在什麼情況下,通過呼叫 GetService 方法來請求服務,您必須一直檢視 GetService 是否返回一個有效的物件。並非所有服務在任意平臺中都是可用的,並且服務一次可用並不代表它將來也可用。因此,通常通過禁用請求服務的功能,編寫降低等級的程式碼,以防止服務變得不可用。
如果新增一個服務,請謹記,一旦處置設計器即移除該服務。設計器宿主可以銷燬,有時還可以重新建立您的設計器。如果無法移除服務,舊的設計器將保留在記憶體中。
返回頁首DesignSurface 和DesignSurfaceManager
.NET Framework 2.0 引入兩個類,用於宿主設計器併為設計器提供服務:DesignSurface 和 DesignSurfaceManager。DesignSurface 是使用者眼中的設計器;它是使用者進行操作以更改設計時功能的 UI。DesignSurface 可作為一個單獨的設計器使用,或者可與 DesignSurfaceManager 聯合使用以提供宿主多個 DesignSurfaces 的應用程式的一個公共實現。
DesignSurface 自動提供一些設計時服務(參見圖 4。其中的大部分服務可在服務容器中重寫。替換不可替換的服務是非法的,原因是這些服務的實現均相互依賴。注意,新增到服務容器中(實現 IDisposable)的所有服務將在處置設計表面時進行處置。
除預設服務之外,DesignSurface 還提供 IDictionaryService,在元件的任意場合均可用。該服務提供鍵/值對的常規字典,該鍵/值對可用於儲存有關元件的任意資料,並且該服務對於每個元件而言是唯一的。這些服務不可能進行替換,因為無法根據場合替換服務。
DesignSurfaceManager 旨在成為設計器的容器。它提供常規服務,用於處理設計器、屬性視窗和其他全域性物件之間的事件路由。DesignSurfaceManager 的使用是可選的,但建議在有若干設計器視窗的情況下使用它。
DesignSurfaceManager 還自動提供一些設計時服務(參見圖 5)。其中的每個服務均可通過替換其受保護的 ServiceContainer 屬性中的值進行重寫。對於 DesignSurface,所有新增到服務容器中並且實現 IDisposable 的 DesignSurfaceManager 服務均會在處置設計器應用程式時進行處置。
IDesignerEventService 是一個特別有用的服務。它使應用程式能夠獲悉設計器變為活動的時刻。IDesignerEventService 提供一個設計器的集合,同時它也是存放全域性物件(例如,Property 視窗)的一個位置,並能夠偵聽選擇變化事件。
返回頁首宿主窗體
要演示如何簡便地宿主一個設計器,我編寫了下面的示例程式碼,它建立並顯示了一個基本的Windows® 窗體設計器:
// Create the DesignSurface and load it with a form DesignSurface ds = new DesignSurface(); ds.BeginLoad(typeof(Form)); // Get the View of the DesignSurface, host it in a form, and show it Control c = ds.View as Control; Form f = new Form(); c.Parent = f; c.Dock = DockStyle.Fill; f.Show();
在上面的程式碼片斷中,我利用 Form 載入了 DesignSurface。同樣,您也能夠利用任意具有可用根設計器的元件載入 DesignSurface。例如,可以載入一個 UserControl 或一個 Component。
本文的程式碼示例下載中有四種不同的根元件:Form、UserControl、Component 和 MyTopLevelComponent(一個圖形設計器)。執行該程式碼時,將開啟一個外殼 UI。該介面包括一個工具箱、一個屬性瀏覽器、一個宿主設計器的選項卡控制元件、一個輸出視窗以及一個解決方案資源管理器,如圖 6 所示。從選單中選擇 File | New | Form,用 Windows 窗體設計器開啟一個新的設計器宿主。該操作基本上使用了我為您演示載入設計器的程式碼。不僅僅是載入一個 Form,示例應用程式演示如何載入一個 UserControl 或 Component。
圖 6 宿主 Windows 窗體設計器
要建立一個根元件,首先建立實現 IRootDesigner 的設計器,然後將該設計器與元件進行關聯。根元件的 View 屬性指定將呈現給使用者的檢視。
由 DesignSurface 提供的一個主要服務是 IDesignerHost。它是用於提供設計器和對型別、服務和事務進行訪問的主要介面。它還可用於建立和銷燬元件。要向我之前已建立的 Windows 窗體設計器新增一個按鈕,只需從 DesignSurface 獲得 IDesignerHost,然後用它建立如圖 7 所示的按鈕。
IToolboxUser 指定設計器支援從工具箱中新增控制元件。即,如果有一個實現 ToolboxService 的工具箱,您可使用 IToolboxUser 介面將控制元件新增到根元件。例如:
/* Add a Button to the Form using IToolboxUser */ IDesignerHost idh = (IDesignerHost)ds.GetService(typeof(IDesignerHost)); IToolboxUser itu = (IToolboxUser)idh.GetDesigner(idh.RootComponent); itu.ToolPicked(new ToolboxItem(typeof(Button)));
當通過雙擊工具箱中的專案將控制元件新增到示例應用程式中的自定義 RootDesigner 時,RootDesigner 的檢視進行更新以顯示如圖 8 所示的餅圖。單擊 GraphStyle 連結,將檢視改為條形圖。
圖 8 自定義 RootDesigner 更新
返回頁首
工具箱
MyRootDesigner 實現 IToolboxUser 介面。有兩種方法:GetToolSupported 和 ToolPicked。可以使用 GetToolSupported 篩選可新增到設計器的專案。最後,ToolPicked 呼叫 ToolboxItem 的 CreateComponents 方法(顧名思義,該方法用於建立元件)。
既然已經向設計器添加了控制元件和元件,那麼讓我們更細緻地看看如何實現工具箱。首先,工具箱需要實現 IToolboxService — 該服務新增到服務容器,並且可由任何需要使用它的使用者訪問。IToolboxService 的主要功能如圖 9 所示。
要使工具箱中的專案可通過滑鼠或鍵盤新增到設計器,則可以將示例中的工具箱掛鉤到 KeyDown 和 MouseDown 事件。對於 Enter 鍵或滑鼠雙擊,呼叫 IToolboxUser.ToolPicked。該示例說明,在滑鼠單擊事件發生時,如何將 ToolboxItem 序列化到 DataObject 和 DoDragDrop 中。將在鬆開滑鼠鍵時呼叫 IToolboxService.SerializeToolboxItem,然後將專案新增到設計器。
當一個新的控制元件或元件新增到設計器時,您可以為由 INameCreationService 實現的控制元件提供一個自定義的名稱。該示例應用程式通過使用 CreateName、ValidateName 和 IsValidName 說明該服務的一個活動示例。
返回頁首多種DesignSurface
管理多個 DesignSurface 時,一個好辦法就是使用 DesignSurfaceManager。它能夠更輕鬆地管理這些 DesignSurfaces。(請注意,DesignSurfaceManager 的服務還可用於 DesignSurface。)
DesignSurfaceManager.CreateDesignSurface呼叫將呼叫 CreateDesignSurfaceCore。您可以重寫該函式來建立一個自定義的 DesignSurface 並新增服務。該示例應用程式通過重寫 HostSurfaceManager 類中的該函式建立一個自定義的 HostSurface:
protected override DesignSurface CreateDesignSurfaceCore( IServiceProvider parentProvider) { return new HostSurface(parentProvider); }
然後,您可以掛構到 ActiveDesignSurfaceChanged 事件,並更新 HostSurfaceManager 類中的輸出視窗,如下所示:
void HostSurfaceManager_ActiveDesignSurfaceChanged( object sender, ActiveDesignSurfaceChangedEventArgs e) { ToolWindows.OutputWindow o = this.GetService(typeof(ToolWindows.OutputWindow)) as ToolWindows.OutputWindow; o.RichTextBox.Text += "New host added./n"; }返回頁首
DesignerLoaders
迄今為止,我們已經建立了 DesignSurfaces,宿主了設計器,添加了控制元件,實現了工具箱並新增、訪問了如 OutputWindow 這樣的服務。下一步是持久保持設計器。如您所願,設計器載入器用於從某些持久狀態載入設計器。簡單又靈活,設計器載入器只有少量需求。實際上,您只需一行設計器載入器程式碼(只需建立 System.Windows.Forms.Form 的一個例項)即可建立 Windows 窗體設計器的一個例項。
除了載入窗體設計,設計器載入器還可以儲存設計。因為儲存是可選操作,所以設計器載入器要進行偵聽以改變設計器宿主的事件,然後自動儲存與這些事件相關的狀態。
.NET Framework 2.0 引入兩個新類,用於編寫自定義的載入器:BasicDesignerLoader 和 CodeDomDesignerLoader。示例應用程式說明這兩個載入器型別的實現。以前,我演示過通過傳遞元件的型別來載入 DesignSurface 的根元件。然而,如果您使用載入器,則它可用於載入設計表面。當使用載入器時,將使用如下所示的 BeginLoad 程式碼片斷:
// Load it using a Loader ds.BeginLoad(new MyLoader());
DesignerLoader 用於載入 DesignSurface 中的根元件,以及建立任意元件。建立一個新窗體或任意其他根元件時,只載入載入器。對比一下,當從程式碼檔案或其他儲存進行載入時,載入器用於分析檔案或儲存,重新建立根元件以及任何其他需要的元件。
.NET Framework 定義一個名為 DesignerLoader 的抽象基類,它用於載入和儲存持久儲存的設計器。該基類是抽象的,因此可使用任意型別的永續性模型。然而,它也增加了該類實現的複雜性。
BasicDesignerLoader 提供一個完整且通用的設計器載入器實現,但不包括與永續性格式相關的資訊。象 DesignerLoader 一樣,它是抽象的,不表示任意有關永續性格式的資訊。然而,BasicDesignerLoader 所作的就是處理一些標準工作,如瞭解何時進行儲存,瞭解如何重新載入,以及跟蹤設計器的更改通知。它的功能還包括,支援多個載入依賴項,跟蹤修改過的位以指示需要儲存變更,延緩重新載入支援的空閒時間。
圖 10 所示的服務通過 BasicDesignerLoader 新增到設計器宿主的服務容器。對於其他服務,可通過在受保護的 LoaderHost 屬性中編輯其值來更改可替換的服務。該示例應用程式實現一個 BasicDesignerLoader,它以 XML 格式保持狀態。要了解它如何實現,選擇 File | Type | BasicDesignerLoader。然後,選擇 File | New | Form 建立一個新窗體。要檢視生成的 XML 程式碼,選擇 View | Code | XML。由應用程式生成的 XML 程式碼,如下所示:
<Object type="System.Windows.Forms.Form, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" name="Form1" children="Controls"> <Property name="Name">Form1</Property> <Property name="DataBindings"> <Property name="DefaultDataSourceUpdateMode">OnValidation</Property> </Property> <Property name="ClientSize">292, 273</Property> </Object>
PerformFlush 和 PerformLoad 是 BasicDesignerLoader 的兩個抽象函式,分別用於實現序列化和反序列化。
返回頁首CodeDomDesignerLoader
設計時序列化由生成的原始碼處理。程式碼生成方案的困難之一是多對語言的處理。.NET Framework 旨在與各種語言配合,因此我還希望設計器能生成一些其他語言。要解決這個問題,有兩種方法。第一,需要每個語言提供商編寫其語言的程式碼生成引擎。遺憾的是,沒有一個語言提供商能預見到如此眾多的程式碼生成需求,這些需求都是第三方元件提供商需要的。第二種方法是,需要每個元件提供商為他們要支援的每種語言提供程式碼生成。這同樣也很糟糕,因為所支援語言的數量並不固定。
要解決這一問題,.NET Framework 定義一個名為程式碼文件物件模型(Code Document Object Model,CodeDOM)的物件模型。所有原始碼基本上均可拆分為基元元素,並且 CodeDOM 是這些元素的一個物件模型。當代碼符合 CodeDOM 時,生成的物件模型可以稍後傳送到特殊語言的程式碼生成器,以呈現適當的程式碼。
.NET Framework 2.0 引入 CodeDomDesignerLoader,它從 BasicDesignerLoader 繼承而來。CodeDomDesignerLoader 是一個完整的載入器,在讀和寫 CodeDOM 時使用。它是一個 turnkey 設計器載入器,所以您只需提供 CodeDOM.。
在示例應用程式種,您可以選擇 File | Type | CodeDomDesigner-Loader 來檢視一個 CodeDOM 活動的示例。通過選擇 File | New | Form 建立一個新窗體 — 建立一個 DesignSurface 並通過 CodeDomDesignerLoader 進行載入。要檢查程式碼,選擇 View | Code | C# 檢視窗體程式碼的 C# 版本,或選擇 View | Code | VB 檢視 Visual Basic® 版本。
要生成程式碼,示例應用程式使用 CSharpCodeProvider 和 VBCodeProvider。它還使用程式碼提供程式編譯程式碼,並執行可執行檔案(參見圖 11)。
ITypeResolutionService(使用 CodeDomDesignerLoader 時需要它)用於解析型別。例如,一種情況是,當從工具箱向設計器新增控制元件時,呼叫該服務以解析型別。示例應用程式解析了 System.Windows.Forms 程式集的所有型別,因此,您可以從 Windows 窗體的工具箱選項卡新增控制元件。
返回頁首小結
正如您看到的,.NET Framework 提供了一個強大、靈活的設計器宿主基礎結構。設計器提供了直觀的可擴充套件性,可幫助解決特定的需求,或較之早期 Visual Studio 版本所支援的更高階的方案。當然,設計器也可以輕鬆地宿主到 Visual Studio 外部。.請確保下載了示例應用程式,這樣,您就可以試執行程式碼並開始實現您自己的自定義設計器。
Dinesh Chandnani 是一位軟體設計工程師,在 Microsoft 的 .NET Client 小組中負責測試,工作涉及設計器宿主和其他設計器功能。他於 2002 年碩士(亞利桑那大學的電腦科學碩士學位)畢業之後即在 Microsoft 工作