UML软件工程组织

為 .NET Framework 應用程式實作類似 Microsoft Word 的物件模型

 

作者:Omar AL Zabir 出处:csdn

 

下載本文的範例程式碼 OfficeAutomation.msi

摘要:本文介紹一種遵循「模型-檢視-控制器」(Model-View-Controller) 設計模式的方法,為 .NET Framework 應用程式實作類似 Microsoft Word 的物件模型。(此文章包含連至英文網頁的連結,列印共 26 頁)

目錄

所有的 Microsoft Office 應用程式皆有相同的物件模型建置基礎,能支援自動化 (Automation)。任一位開發人員都能利用物件模型,來推動應用程式的使用者介面,進行內容的新增、編輯和刪除等工作,就好比一位使用者實際與應用程式進行互動一般。此豐富的物件模型搭配上自動化的支援,就能使得 Office 應用程式成為名副其實具有可延伸性與可外掛性的應用程式。無論是誰,都能在短時間內依自己的需求撰寫出強大的增益集,延伸 Microsoft Word 的行為。而好的物件導向 (Object-oriented,OO) 開發人員,會遵照「模型-檢視-控制器 (MVC)」設計模式,以豐富的架構及合理的物件導向來開發自己的應用程式。

然而,至今為止,已開發的應用程式中具有像是 Microsoft Office 應用程式自動化功能的可說是寥寥無幾,導致我們無法像延伸和自訂 Office 應用程式一樣,利用 .NET Framework 和 Microsoft Visual Basic for Applications (VBA) 來延伸我們的應用程式。本文會向您展示一種方法為 .NET 應用程式實作類似 Microsoft Word 的物件模型,並大量採用「模型-檢視-控制器」設計模式以及 .NET Framework 事件與委派。文內開發出的物件模型能夠為應用程式添加無限的延伸性,讓我們有機會在自己的應用程式裡加入增益集和指令碼功能,有如產品本身的設計一般,而無須特地撰寫額外程式碼。增益集與指令碼功能的威力將與核心應用程式不相上下。這種設計還會生產出十分簡潔的程式碼基底,確實將商務邏輯從 UI 邏輯中抽離出來。而最棒的一點是,我們可以撰寫程式碼來驅動 UI,藉此建立出不僅能測試商務邏輯,還能測試 UI 行為的指令碼。

Microsoft Word 物件模型階層架構概觀

我們先來看看 Microsoft Word 的物件模型。若您對 Microsoft Word 物件模型的運作方式已十分熟悉,可直接閱讀下一節內容。

Microsoft Word 的支援自動化物件模型的起點就是 Application 類別。一個 Word 執行個體具有一個能提供工具列、功能表、狀態列等的單一 (Singleton) 應用程式物件。

[圖 1] Microsoft Word 物件模型 (部分模型)

這個 Application 類別可以驅動整個應用程式; 我們能存取任何文件,管理其內容,新增命令列,變更功能表標籤和圖示,撰寫程式來按下一個按鈕,顯示狀態訊息等等。舉例而言,您可以呼叫 Application.Documents.Open(),在 Word 裡顯示一個 [開啟舊檔] 對話方塊;呼叫 Application.Quit(),來結束 Word。只要撰寫程式碼,Microsoft Word 就會依您的命令行事。您自己還有 Word 的開發人員不用再尋找能顯示文件的模組,或是呼叫其 public 方法的模組,告訴它要變更文件名稱,現在只要取得 Document 物件的執行個體,然後更改它的 Name 屬性就行了。接著 UI 會立即反映出您所做的變更,而這正是能支援自動化的物件模型之妙處所在。

以前在還沒有對我自己的應用程式實作支援自動化的物件模型時,我時常敬佩地望著 Visual Studio IDE,對它讚嘆不已,心理納悶著 Visual Studio 的開發人員究竟是如何追蹤某事件這麼多層面的。舉例來說,想想看當按下 DEL 按鍵時,將某個檔案從 [方案總管] 樹狀目錄裡刪除的那一刻,有多少事情同步發生。樹狀節點被移除,專案和方案改設為擱置儲存模式 (變更),視窗的標題列產生變化,狀態列改變,要是文件剛好處於開啟狀態,則其標籤會被關閉,功能表列會被停用等等,整個 IDE 經歷大幅的變動。想想看那個實作這個檔案刪除功能的可憐傢伙,他可能完全無法想像,執行所有這些動作就只為了一個檔案刪除功能?實際上,他一點都不必擔心,因為 Visual Studio 具有一個超炫的支援自動化物件模型。學會這些物件模型之後,我才知道,這種應用程式其實沒我想像中的複雜。

物件模型搭配自動化支援的好處

現在我們來淺談一下自動化支援物件模型究竟有哪些好處。當您的應用程式有了這種物件模型之後,請牢記下列幾個原則:

  • 不要一直掛念著 UI。
  • 把焦點放在一次只進行小單位的工作。
  • 讓每個模組進行自己的工作。

不要一直掛念著 UI

我瞭解程式設計人員在接到新的任務時,總是把 UI 當成首要焦點工作。如果您請一位程式設計人員將檔案載入某資料夾內,再把它以樹狀檢視方式顯示出來 (像是 Windows 檔案總管一樣),他通常會想到下列幾個問題:

[圖 2] Windows 檔案總管以樹狀檢視顯示檔案與資料夾

  • 如何讀取檔案系統?
  • 如何填入樹狀檢視節點?
  • 如何從要執行填入的那個類別聯繫到樹狀檢視?
  • 如何將樹狀檢視的參考傳遞到前面提到的那個類別?
  • 如何將檔案系統內的變更反映到樹狀檢視?
  • 如何在使用者變更樹狀檢視節點後,更新物件模型?

您看看,程式設計人員的考量多半是擔心如何才能將某個動作反映在 UI 上。您可以告訴程式設計人員只要把檔案和資料夾填入一個集合類別就好,一個物件一旦加入到集合內,它就會很神奇地自動顯示在樹狀檢視裡面,這樣他就可以無牽無掛地專心在自己的任務上,把應用程式的其他部分完全拋之腦後。

一套受到支援自動化的物件模型可以讓開發人員從任務開始到任務結束,從資料存取層到前端,都不因任務的實作而分心,反到可以專心工作,不受到系統其他方面考量的牽絆。

把焦點放在進行小單位的工作

擁有一套支援自動化的物件模型後,您的設計與實作流程就會縮小為小單位工作。舉例而言,您會先思考如何把檔案從檔案系統填入到 Application.Files 集合,但不必知道應用程式內有沒有樹狀檢視可以顯示檔案,您要不斷地提醒自己:沒有 UI,沒有 UI。應用程式內可能連一個樹狀檢視都沒有,也可能有三個樹狀檢視以及四個需要以同樣檔案清單填入的清單方塊,但您不需要知道到底有幾個,只需要想想要如何讀取那些檔案,然後呼叫 Application.Files.Add( new File( ... ) ) 來填入集合。

[圖 3] Visual Studio .NET 方案總管

現在讓我們來探討 UI 方面的任務。假設您正在建置一個類似 Visual Studio 的應用程式 (我不是在說笑喔!)。負責建置應用程式的「方案總管」的開發人員知道,每次在 Application.Files 集合內加入一個項目,就必須把它加到方案總管樹狀結構裡面。他不需要瞭解檔案物件究竟源於何處,它有可能是來自檔案系統,也有可能是使用者新增到檔案系統中的檔案,更有可能是由協力廠商增益集或是某個巨集程式所新增的。再次強調,方案總管開發人員知道,如果使用者從 Application.Files 集合刪除一個項目,就必須把該項目從檔案系統內刪除掉。但要做的可能不僅於此,因為該檔案有可能是不在檔案系統中的邏輯檔案,它有可能是增益集所產生的一個檔案。開發人員唯一要瞭解的是,使用者每次刪除 File 物件後,就必須將該物件一併移除。而其他負責將該檔案物件加入集合的人早已注意到物件的變更,而且會在物件遭到移除時,見機行事採取適當的行動。

讓每個模組進行自己的工作

有了支援自動化的物件模型後,就能將某些功能實作的責任分派給負責不同模組的人員,完全不用考慮他們是否知道其他人的存在。換言之,負責方案總管的人不必知道有人專門負責文件索引標籤,需要在某個檔案在方案總管內被按兩下後,啟動一個索引標籤。而負責文件索引標籤的人也無須瞭解有一個專門負責視窗功能表的人,需要顯示所有開啟的文件索引標籤。他們只要知道有一個物件模型,裡面有模型物件,會在發生事情時通知他們,這樣就夠了。

試想,當您從方案總管樹狀結構內刪除檔案時,會發生什麼事。視窗功能表內的檔案名稱必須要移除,如果該檔案處於開啟狀態,就必須關閉文件視窗,而且檔案也得從檔案系統中移除。另外專案也需要標示為「變更 (Dirty)」或儲存擱置。如果沒有開啟中的文件,就必須停用功能表、停用命令列等等。當使用者對一個檔案按下 DEL 按鈕時,要處理的事情太多了。以前在不清楚支援自動化的物件模型概念時,我會撰寫下列程式碼:

AskForSave( file );
DeleteTreeNode( file );
CloseOpenDocument( file );
RemoveMenuItem( windowMenu, file );
UpdateRecentFiles( fileMenu, file );
KillFile( file );
MakeProjectDirty( project );

而在實作任何功能時,無論多麼簡單的功能,我都要在腦子裡想像整個應用程式。當然啦,我還會使用適當的命令模式、設計完善的類別來完成工作,以及高度模組化的應用程式。反正,整個實作一直在我腦子裡徘徊,而隨著模組數目日趨增多,壓力也越來越大。

但是有了支援自動化的物件模型,就能採用下面的方法:

  1. 文件檢視器會等候檔案已移除的相關通知,然後關閉顯示該文件的索引標籤。
  2. 方案總管樹狀結構會等候檔案已移除的相關通知,然後把檔案從樹狀結構中移除。
  3. 檔案功能表收到通知,然後移除檔案項目。
  4. 視窗功能表收到通知,然後從清單中移除檔案名稱。
  5. 檔案系統管理員收到通知後,刪除檔案。
  6. 一些正在執行,而且將自己附在 Application.Files 集合中的外掛程式,會收到移除通知,然後適當行動。
  7. 狀態列收到通知,然後顯示訊息。

應用程式裡不同的模組會訂閱不同的物件與集合 (由 Application 物件公開),並會聆聽與它們有關的通知。收到通知後,它們只會進行與自己相關的作業,並忽略其他通知。對於其他模組的存在與否並不在意。

與模型-檢視-控制器模式之比較

凡是瞭解 MVC 設計模式的人肯定會喃喃自語地說,有什麼了不起,不就是 MVC 而已!這類的物件模型確實遵守「模型-檢視-控制器」的原則,也就是:

  • 您有一個模型,內含您的資料,例如,「車子」是一個模型。
  • 您有各種控制器,可對模型進行變更,例如,「駕駛員」就是開車的控制器。
  • 您有各種檢視,可在 UI 上顯示輸出結果,「控制器」會使用「檢視」來顯示模型輸出,例如,「油錶」可能就是顯示車子物件裡「燃料」屬性的一個檢視。
  • 控制器和檢視會觀察模型變更,一旦發現異動,檢視就會將變更反映在 UI 上,而控制器則會更新檢視並且 (或是) 更新模型。舉例而言,Car.Started 屬性一變成 True,檢視會發出「隆隆!」的聲音,然後控制器就會設定 Car.Accelerator=1000 來更新車子模型。
  • 檢視收到使用者的動作後,將變更適當反映在模型上,例如,駕駛員把車子熄火,設定就會變成「Car.Started = false」。

[圖 4] MVC 範例物件模型

MVC 是設計桌面應用程式時廣為採用的一種設計模式,您會發現程式的程式碼複雜度在實作 MVC 前後有明顯的差異。http://java.sun.com/blueprints/patterns/MVC.html 中對 MVC 的解釋十分貼切,我在此摘錄兩小段:

當應用程式混合包含資料存取、商務邏輯和展示等三種程式碼時,一些問題就會隨之而來。由於各種元件的相依性高,進行一項變更後,會發生牽一髮動全身的效應,因此要維護這種應用程式十分棘手。而類別之間因為嚴重相互依賴,實在不太可能可以重複使用。要想新增資料檢視,通常需要重新實作或剪貼商業邏輯程式碼,這不免就需要在多處地方進行維護工作。資料存取程式碼因為是從商務邏輯方法剪貼而來,固也有同樣的問題。
「模型-檢視-控制器」設計模式透過將資料存取、商務邏輯還有資料展示與使用者互動全部分離,進而解決了這些問題。

在我們的物件模型裡面,Application.Files 集合是模型,「方案總管」模組是控制器,而顯示檔案的樹狀檢視是檢視。然而,一般 MVC 與此模型的基本差異在於,控制器對檢視一無所知。我在文章前面已經說過,「不要一直掛念著 UI」,控制器根本就不知道有檢視這個東西。它所要進行的事項,全都是在模型內完成。模型身處檢視與控制器之間,將它們完全分離,並提供一套能讓開發人員可以仔細思考、專心開發系統,而不再需要擔心 UI 的架構。

[圖 5] 根據我們模型修改後的 MVC

另外,我們通常不會建立一個從單一類別 Application 開始的物件模型階層架構,然後再提供整個 UI 的完整對應。一般而言,我們的物件模型會含有與我們商務網域相關的實體物件。舉例而言,「人」、「帳戶」、「交易」等等,都是我們常透過物件模型公開的物件。在正常的情況下,物件模型並不包含「按鈕」、「工作列」、「功能表」、「狀態列」等。如果我們建立一個不但能反映商務物件,還能反映 UI 結構的物件模型,並且讓我們的 UI 模組能對物件模型所執行的動作做出回應,這樣就能建立出一個可提供自動化的物件模型。這正是 Microsoft 產品在其應用程式內所採用的原則。藉此,我們得以延伸它們的應用程式,還能全權掌控內含物件的資料以及 UI。我們會將同樣的方法運用到我們的應用程式裡,利用 .NET Framework 事件與委派,建立出能提供支援自動化的物件模型,再建立一個範例應用程式,來看看最終結果。

Smart Editor 的製作

本文所附的範例專案是一個叫做「Smart Editor」(智慧型編輯器) 的文字編輯器,它之所以「有智慧」,是因為它具有一個支援自動化的可延伸兼可外掛之物件模型。這個應用程式看此來與 Visual Studio IDE 十分類似。而物件模型也是以 Visual Studio 本身的物件模型 (與 Microsoft Word 物件模型很像) 為基礎演變而來的。令人訝異的是,所有 Microsoft 的產品在其物件模型方面,概念都十分相近。閱讀完本文章後,您也會明白為何所有產品都有同樣的物件模型設計構思,以及這種設計的方便性。

[圖 6] Smart Editor 的 UI

支援自動化的物件模型與應用程式物件模型

此應用程式具有一個小小的物件模型,能支援像是 Microsoft Word 的自動化。根物件是 Application。請不要把這裡的 Application 類別與 .NET Framework 的 Application 類別混為一談,System.Windows.Forms.Application 是 Windows Form 的類別,而我們的 Application 類別是 Editor.ObjectModel.Application。若想在您的程式碼內將我們的「Application」類別用作為預設類別,來取代 Windows Form 的類別,只要在檔案開頭進行下面宣告就可以了:

using Application = Editor.ObjectModel.Application;

本文中接下來提到的「Application」是指 Editor.ObjectMode.Application,而非 Windows Form 命名空間裡的那一個。

[圖 7] Smart Editor 物件模型

這些物件都各自對應到特定的 UI 項目,下圖會解說 UI 的組織方式。

[圖 8] Smart Editor 模組

[圖 8] 把整個 UI 按使用者控制項分成幾個部分。每個部分都代表一個使用者控制項,這個 UI 裡最重要、也是我在文章中會不段提到的,就是右上角的 Document Explorer。另一個會常說到的就是文件標籤,亦為顯示文件內容的文字方框。

Application

這是整個物件模型的單一類別和根目錄。物件模型是由它的建構函式所啟動:

 

private Application()
{
   this._Tabs = new TabCollection( this );
   this._ToolBars = new ToolBarCollection( this );
   this._Menus = new MenuCollection( this );
   this._Documents = new DocumentCollection( this );
}

建構函式主要扮演的角色,是啟動物件模型階層內的第一層物件。

這些集合都各自對應到一些 UI 項目,只要 UI 一載入,馬上就會填入 ToolbarsMenus 集合。

[圖 9] 對應 UI 項目與物件模型

Document 集合

Application 類別會公開一個名為 Documents 的公用集合,而這個集合是 DocumentCollection 的執行個體。它是從一個名為 SelectableCollectionBase 的自訂集合類別衍生出來的。它會公開一個 Selected 屬性,您可隨時從其中取得目前所選文件。它和 ListboxTreeview 控制項的 Items 集合十分類似,只是這個集合裡有一個能傳回目前所選項目的 Selected 屬性。文章稍後會詳細說明這種運作方式。

由於大半部分的工作是 SelectableCollectionBase 在處理,因此 DocumentCollection 部分的程式碼很簡短,它只是提供一些函式,讓 Document 類別成為強式型別:

public class DocumentCollection : SelectableCollectionBase
{
   new public int Add( Document doc )
   {
      return base.Add( doc );
   }

   new Document this[ int index ]
   {
      get { return (Document)base[ index ]; }
   }

   new public Document Selected
   {
      get { return (Document) base.Selected; }
      set { base.Selected = value; }
   }

   public Document New( string name, string path, byte [] data, IDocumentEditor editor )
   {
      return new Document( name, path, data, editor, null );
   }

   public DocumentCollection( object parent ) : base( false, parent ) { }
}

唯一的要求是,您要以 true/false 來呼叫 base( IsMultiSelect, ParentObject ),如此方能指明它是否為複選集合,並識別它上一層的集合是什麼。為了要維護這個階層,子物件一定會含帶父物件的弱式參考。

Document 類別也很簡單,畢竟它是 ItemBase (能公開這類物件模型所有所需的功能) 的延伸產物 (稍後詳述)。您只要呼叫 base(parent),然後傳遞父物件的參考即可:

public class Document : ItemBase
{
   private string _Name;
   private string _Path;
   private byte [] _Data;
   private IDocumentEditor _DocumentEditor;

   public IDocumentEditor DocumentEditor { ... }

   public byte[] Data  { ... }

   public string Name { ... }

   public string Path { ... }

   public Document( 
      string name, string path, byte [] data, 
      IDocumentEditor editor, object parent ) 
      : base( parent )
   {
      ...
   }

}

Menu 集合

Menu 集合很像是 Menu 類別的一個集合,而由於我們對維護所選的功能表一事並沒興趣,它繼承的是 CollectionBase 而不是 SelectableCollectionBase。不過若要維護的話,只要繼承 SelectableCollectionBase (不是 CollectionBase),即可輕鬆辦到。

一旦 UI 載入後,它會為功能表列中的每個功能表建立一個 Menu 物件,舉例而言,[檔案] 、[編輯]、[檢視]、[工具] 等都是功能表,而它們在 Application.Menus 集合裡都有一個自己的物件。

MenuItem 集合

每個功能表都具有一個 MenuItem 集合,功能表項目都是儲存在此集合中。例如:

MenuItem fileNew = Application.Menus[ "File" ].Items["New"];
fileNew.Click();

這會傳回「檔案 -> 開新檔案」功能表項目之代表物件的參考。之後呼叫其 Click 方法,即可模擬按下一個功能表項目的動作,彷彿使用者已經按下該功能表項目。

Tab 集合

文件是以索引標籤的方式顯示,每個文件都會有一個索引標籤,裡面包含文件編輯器,但 Application 類別的 Tabs 集合裡可用的只有開啟的標籤。

[圖 10] 索引標籤的物件模型

每當開啟檔案要進行編輯時,集合中就會新增一個新建的索引標籤,當該文件關閉後,Tab 物件就會從集合中移除。

您隨時都可以呼叫 Tab 物件的 Show 方法,將該索引標籤顯示到螢幕上,亦可呼叫 Close 方法,將其關閉。

Toolbar 集合

UI 上的每個工具列,在 Applications.Toolbars 集合裡都有對應且可用的一個 Toolbar 類別執行個體。您可從 Application.Toolbars[ index ] 取得工具列參考,也可使用 Toolbar 集合的 Add 方法,在 Run Time 加入新工具列。

工具列含有一個工具列項目集合,這些項目有可能是按鈕、下拉式清單、分隔符號等等。就每個工具列項目而言,Toolbar 類別的 Items 集合裡都有一個可用的 ToolbarItem 執行個體。使用下面程式碼,即可取得 New 按鈕的參考:

// Get the first toolbar in the collection and the first toolbar item
ToolbarItem newButton = Application.Toolbars[ "Standard" ].Items[ "New" ];
newButton.Click();

接下來,可呼叫 Click 方法來模擬按下按鈕的動作。

物件模型的使用範例

您可隨處呼叫 Application.Quit(),來結束應用程式,也可呼叫 Application.Save(),來儲存目前開啟中的文件 (如果有的話)。Application.ActiveDocument 屬性固定會傳回目前選定要進行編輯的文件,或是從 Document Explorer 內所選取的文件。您還可呼叫目前有Application.ActiveTab 焦點的索引標籤。

展示層如何使用物件模型

範例應用程式說明了一種方法,讓您使用支援自動化的物件模型,來設計自己的應用程式。實作方法很簡單,但為了單純起見,我也省略了許多最佳實作方法。請不要以為這是利用此種物件模型來建置應用程式的唯一之道。

主表單

在主表單載入後,會先訂閱所有 Application 類別公開的 UI 相關泛用事件,像 Application.OnFileSaveDialogApplication.ShowStatus 等等都是。任何人都能提供這些常見且泛用的 UI 服務,而本例中,即主表單,它所扮演的角色是 Application 類別的集中式 UI 服務提供者。當然啦,如果應用程式很複雜,就會把比較重要的責任分配給小一點的模組。不過在這個範例應用程式裡,我們將會從主表單提供所有必要的泛用 UI 支援。舉凡所有需要在 UI 內顯示訊息、變更應用程式視窗標題列、或是顯示通用檔案對話方塊等情況,主表單都會回應 Application 類別所引發的這些事件,並依情況行事。

上層區域

這個控制項掌管功能表與工具列,雖然它們兩者都是在設計階段準備的,但卻是在執行階段時利用 MenuToolbarToolbarItems 填入物件模型,使 UI 項目得以自動化使用。

首先,它會建立第一個工具列的 Toolbar 物件,程式碼非常簡單:

standardToolBar = Application.ToolBars.New( "Standard" );
Application.ToolBars.Add( standardToolBar );

接著,它為工具列上的每個按鈕建立 ToolbarItems

ToolBarItem itemNew = standardToolBar.Items.New( "New", string.Empty, btnNew );
standardToolBar.Items.Add( itemNew );
itemNew.OnClick += new ToolBarItemClickHandler(itemNew_OnClick);

同理,它也會為每個功能表建立 MenuMenuItem 物件。舉例而言,File 功能表的建立方式如下:

Menu fileMenu = Application.Menu.New( "File", string.Empty, this.mnubarStandard, null );
Application.Menu.Add( fileMenu );

「上層區域」控制項身負一些重要的責任,像是:

  1. 聆聽物件模型,注意 MenuMenuItemToolbarToolbarItems 裡面有哪些變更。只要一有變更,例如,如果某個按鈕被停用或是標題有所改變,它會將該變更反映在 UI 上。比方說,Toolbar 物件提供一個 OnChange 事件,一旦有任何工具列項目遭到修改,就會觸發這個事件。而工具列主控模組會接收此事件,再將變更反映到 UI 上。
    private void toolbar_OnChange(ItemBase item, StringArgs s)
    {
       if( item is ToolBarItem )
       {
          ToolBarItem toolBarItem = item as ToolBarItem;
          
          ButtonItem button = toolBarItem.Tag as ButtonItem;
          
          button.Enabled = toolBarItem.Enabled;
          button.Visible = toolBarItem.Visible;
          button.Checked = toolBarItem.Selected;
       }
    }
    
  2. 聆聽物件模型,注意新增或移除的 MenuMenuItemToolbarToolbarItems 有哪些。其他模組隨時都可能在物件模型內新增工具列,那樣一來,UI 上也需要以適當的設計項目來建立 Toolbar。同理,如果將 MenuItem 從任一個 Menu 物件的 Items 集合中移除,該功能表項目也須自 UI 上移除。

每個需要提供自動化的模組,都必須提供這兩個服務以便將物件模型上的將動作反映到 UI,以及將 UI 上的動作反映到物件模組。

中間區域

這個控制項主控著開啟文件的索引標籤,它會聆聽 Application.Documents 裡面所做的變更,還有,最重要的是它會聆聽 Applications.Tabs 裡面的變更。每當 Application.Tabs 集合中一加入 Tab 物件,Tab 控制項就會隨之建立,當中會包含文件,並顯示在 UI 上。

Application.Tabs.OnItemCollectionAdd += new CollectionAddHandler( Tabs_OnAdd );
...
...
private void Tabs_OnAdd( CollectionBase collection, ItemBase item )
{
   if( item is Tab )
   {
      CreateNewTab( (Tab) item );
   }
}

它也會聆聽任何 Document 物件的 Show 方法呼叫,因為如果有人呼叫 doc.Show(),就需要建立一個新的索引標籤,並把它加到 Tabs 集合,以便將Document 顯示在編輯模式中。

Document Explorer 樹狀目錄

使用者每次建立的新檔案或開啟的文件,都會加入 Document Explorer 樹狀目錄內。這個樹狀目錄代表 Application.Documents 集合,它所執行的基本任務有二:

  1. 聆聽 Application.Documents 集合裡所做的變更。只要一新增、編輯或刪除項目,它就會回應變更,並對樹狀目錄進行適當的工作。舉例而言,一旦 DocumentCollection 裡新增一個 Document,下面程式碼就會開始執行:
    private void Documents_OnItemCollectionAdd(CollectionBase collection, ItemBase item)
    {
       if( item is Document )
       {
          Document doc = item as Document;
    
          DocumentNode node = new DocumentNode( doc );
    
          // There's a tree view control named "treSolution" on the UI
          treSolution.Nodes[0].Nodes.Add( node );
       }
    }
    
  2. 使用者對樹狀目錄執行的任何動作,像是刪除檔案等種種變更都會反映到 Application.Documents 集合中。舉例而言,當使用者按下 DEL 按鈕,就會引發 Treeview Keypress 事件,並以下面的方式處理該事件:
    private void treSolution_KeyUp(object sender, System.Windows.Forms.KeyEventArgs e)
    {
       DocumentNode node = treSolution.SelectedNode as DocumentNode;
       if( null != node )
       {            
          if( e.KeyCode == Keys.Delete )
          {
             Application.Documents.Remove( node.Document );
          }
          else if( e.KeyCode == Keys.Enter )
          {
             node.Document.Show();
          }
       }
       
    }
    

它只不過是把 Document 物件從物件模型中刪除。因為控制項早已注意到集合裡的變更,也已重整樹狀目錄,所以當文件節點從集合中移除時,也會立刻被移除掉。

活動範例執行

下面圖解說明使用者按下工具列的 [開啟舊檔] 按鈕時,會發生什麼事。

[圖 11] 使用者按下 [開啟舊檔] 按鈕後的執行流程

首先,工具列會收到 Click 事件並呼叫 Button 物件 (代表工具列上的 [開啟舊檔] 按鈕) 的 Click 方法。收到通知的按鈕物件附有預設的 Click 處理常式。此處理常式接著會呼叫 Application.Open(),該方法單純只會引發 OnOpen 事件,主表單會擷取此事件,顯示 .NET Framework [開啟舊檔] 對話方塊。當使用者選取某檔案時,會將檔案載入,而且會為該檔案建立新的 Document 物件。一旦 Application.Documents 集合內加入新文件物件,Document Explorer 控制項就會收到通知,並在樹狀目錄內顯示檔案。接下來,會呼叫新文件物件的 Show() 方法,而這會引發文件的 OnShow 事件。負責顯示文件索引標籤的 Document Tabs 模組會擷取該事件,而收到通知後,它會為引發事件的文件建立新 Tab 物件。Application.Tabs 集合裡一旦加入 Tab 物件,Document Tabs 模組也會擷取另一個引發的事件。這個事件會告訴模組,Tabs 集合內還有一個新的 Tab 物件需要顯示。因此,該模組會建立新的 Tab 控制項,主控文字方塊,並且顯示其內的文件內容。

Framework 類別

自動支援物件模型的建置是以下列三種 Framework 類別為基礎: ItemBaseCollectionBase SelectableCollectionBase。這三種類別提供物件模型最重要的一個功能,也就是一個「可觀察式物件模型」。只要從這三種中的其中一個類別繼承類別,您就能觀察到它們的變更。舉例而言,DocumentCollection 類別繼承於 CollectionBaseCollectionBase 含有新增、移除或清除項目時,要引發事件所需的所有程式碼。除此之外,這個集合類別也會傾聽它裡面所有項目所受到的變更。正因此,它能擷取從子物件引發的事件,並將它們轉送到其觀察程式 (Observer)。

[圖 12] 集合項目的事件反昇

上圖說明從子物件到父物件的事件反昇。隨著事件反昇到更上層,無論某集合在階層裡的深淺位置,您都能將事件附在該集合裡,然後接收裡面任何項目所送出的變更通知。

[圖 13] 訂閱集合以取得來自子項目的通知

上圖說明您可訂閱單獨物件來接收通知,亦可訂閱集合來接受其內所有物件送出的通知。

事件會從最底層反昇到最上層。以 [檔案] 功能表的 [結束] 功能表項目為例,每個 MenuItem 都加入到 MenuItemCollection 內,它又會包含在 Menu 物件裡,而此物件又包含在 MenuCollection 中。因此,如果訂閱任何事件的功能表物件,您即可取得其內所有功能表項目的通知。最好的是,如果訂閱了 Application.Menus,就能獲得所有功能表物件的功能表項目所發出的通知。

ItemBase

這個類別是用於像是 DocumentToolbarToolbarItem 等單一物件的類別。唯有在想要從某物件取得變更通知時,才有需要延伸此物件。

這個類別包含一個十分實用的方法,叫做 ListenCollection,它會以某集合為基礎,訂閱其所有的事件。專用於像是 Menu 這種含有其自身集合 (例如,MenuItemCollection) 的複合類別。為了能支援事件反昇,您需要擷取所有子集合類別所引發的事件,來將它們反昇到更高階的物件模型階層。也就是,呼叫 ListenCollection 方法,傳遞您欲傾聽的集合,如此即可接收它的所有事件。而方法很簡單,只要依照原物件引發事件的方式來引發事件就行了。

CollectionBase

這個類別專用於像是 ItemBase 繼承者集合的集合類別,使用起來相當方便,而 DocumentCollectionToolbarCollectionMenuCollection 就屬這種類別。它提供在從子項目反昇事件時所需的所有程式碼,一旦裡面加入一個物件,它就會訂閱 ItemBase 所公開的事件。結果,只要從子物件引發某事件,它就會接收該事件,然後透過其自身事件來引發那個事件。舉例而言,若它接收來自某項目的一個 OnChange 事件,它會引發 OnItemChange(item) 來轉送此事件。這個功能非常實用,您無須訂閱單獨物件,只要訂閱一個集合,就能取得來自它裡面所有項目的事件。

SelectableCollectionBase

這個類別會延伸 CollectionBase,並會公開 Selected 屬性與一個 SelectedItems 集合。ItemBase 包含 Selected 屬性,當它設為 True 且項目是在某個可選取集合裡面時,集合的 Selected 屬性會設定成該項目。該項目也會加入到 Collection 類別的 SelectedItems 集合裡。

選取模式有兩種:「單一選取」和「多重選取」。在單一選取模式裡,只能選取一個項目。一旦它收到選取變更事件,就會取消先前項目的選取模式,將新的項目設為目前選取項目。多重選取就複雜得多,要想瞭解背後的邏輯性,建議您可以研讀程式碼。不過,只要您採用類別繼承,就無須費時深究所有的運作邏輯。

如何建置您自己的應用程式

我們來解說如何開始建置自己的應用程式,以下是步驟解析:

  1. ItemBaseCollectionBaseSelectableCollectionBase 複製到您的專案。
  2. 建立您自己的 Application 類別,此類別必須為單一類別。您可以複製範例類別,然後移除不要的程式碼。
  3. 為像是 Person、Contact、File、Relationship 等的模型建立類別,從 ItemBase 衍生所有單一物件。
  4. 建立 UI 項目類別,例如 Window、Toolbar、Menu、Menuitem、DockingPane 等等。這些類別也必須是衍生自 ItemBase。盡可能地公開屬性,以方便從物件模型來掌控 UI。
  5. 為模型建立集合類別,例如,PersonCollection、ContactCollection 或 WindowCollection。這些類別必須衍生自 CollectionBase SelectableCollectionBase
  6. 公開根物件或來自 Application 類別的集合。舉例而言,公開公用集合,像是 PersonsContactsWindows,並且公開單一物件,像是 ActivePersonCurrentContactActiveWindow 等等。
  7. 使 UI 項目能執行兩個基本任務:

    (1) 傾聽物件模型變更,且將變更反映到 UI 上。舉例而言,當從物件模型的 Button 物件觸發某 OnClick 事件時,將按下的動作反映到 UI 上,讓按鈕看似已被按下。

    (2) 將來自 UI 的事件反映到物件模型。舉例而言,當使用者按下某按鈕時,取得物件模型裡代表按鈕物件的參考,然後呼叫 Click()

外掛程式之介紹

自動化支援物件模型最好的功能之一就是它的可延伸性,甚至能讓您加入外掛程式或是撰寫指令碼。舉例而言,範例程式裡的檔案處理,就是利用外部模組完成的,在執行階段時載入兩個外部模組,好比延伸程式或外掛程式一樣;一個是建立新檔案的 NewFileHandler,另一個是 TextFileLoader,它主要是進行文字檔案載入與儲存的工作。您可能十分訝異其實核心應用程式裡並沒有任何處理「開新檔案」和「開啟舊檔」等功能的程式碼,核心應用程式的功能表模組僅僅建立 NewOpen 的功能表項目,然後在裡面附上一個預設 Click 處理常式來分別呼叫 Application.New() Application.Open()。執行階段外掛程式會於載入時將自己附在 Application.OnNew Application.OnOpen/OnSave 事件,而建立新檔、開啟檔案還有將檔案內容存入檔案裡的功能實際上是由它們提供的。下面程式碼將說明 NewFileHandler 的運作方式:

public class NewFileHandler
{
   public NewFileHandler()
   {
      Application.OnNew += new EventHandler(Application_OnNew);
   }

   private void Application_OnNew( object source, EventArgs e )
   {
      // A new file needs to be opened
      
      // Create a blank document with some dummy text
      byte [] data = System.Text.Encoding.UTF8.GetBytes(string.Empty);

      string newName = this.MakeNewDocumentName();
      Document doc = new Document( newName, string.Empty, data, 
new TextDocumentEditor(), null );
      Application.Documents.Add( doc );

      // Show the editor
      doc.Show();

      Application.ShowStatus( this, "New file created." );
   }
}
// When the main form loads, it is initialized this way
new NewFileHandler();

同樣的,TextFileLoader 外掛程式訂閱了 Application.OnOpenApplication.OnSave 事件,並提供文字檔案的開啟與儲存之功能。您能夠利用同樣的原理,訂閱 Application 類別的 OnOpenOnSave 事件,進而建立 BitmapFileLoaderMusicFileLoader WordFileLoader 等外掛程式。

開發人員若使用物件模型,即可減少核心組件的程式碼篇幅,把多半的程式碼移到執行階段才載入的延伸組件。核心應用程式能保有其簡潔性,且包含極少的商務邏輯。如此一來,就能輕鬆發行應用程式的修補、修正或更新等各種程式,只要送出那些延伸程式的更新版就好了。

指令碼功能

指令碼是所有 Windows 應用程式裡最強大的延伸功能。指令碼能讓您隨時撰寫、執行程式碼,完全掌控應用程式的運作。Visual Studio IDE 具有 [命令視窗],能供一次撰寫一行程式碼,且馬上予以執行。

[圖 14] Visual Studio 命令視窗

您可撰寫 VBA 程式碼來驅動 IDE,讓它依您的需求來執行作業。如此強大的指令碼功能對您的應用程式而言,可大幅提昇可延伸性。您從前文已瞭解,只要呼叫來自 Application 的方法與其子物件,就能隨心掌控 UI 的行為。若能在執行階段執行一些 C# 程式碼,就能提供指令碼功能。使用者能利用物件模型撰寫程式碼,而您則需要於執行階段執行它們。

範例應用程式示範了方法為何。使用 System.CodeDom.Compiler 命名空間,即能產生、編譯 C# 程式碼,之後於執行階段利用我們的程式碼來執行程式碼,如下所示:

using( CSharpCodeProvider provider = new CSharpCodeProvider() )
{
   ICodeCompiler compiler = provider.CreateCompiler();
   ...
   string fullSource = this.MakeCode( code );
   CompilerResults results = compiler.CompileAssemblyFromSource(options, fullSource);
   ...
   try
   {
      Assembly assembly = results.CompiledAssembly;
      Type type = assembly.GetType( NAMESPACE_NAME + "." + CLASS_NAME );

      object obj = Activator.CreateInstance( type );
      MethodInfo method = type.GetMethod(METHOD_NAME);
      method.Invoke( obj, null );
   }
   ...
}

我們能提供使用者一個文字方塊來收集要執行的程式碼,然後將程式碼包裝在某類別的一個方法裡面,把它編譯成一個組件。之後我們會得到類別,然後再以反映方式動態呼叫方法。

Smart Editor 的命令視窗如下所示:

[圖 15] Smart Editor 命令視窗

您可在裡面撰寫程式碼,然後一次執行所有的程式碼。您不妨試試當中已有的程式碼,將能目睹它建立一個具有 10 個按鈕的工具列,並開啟 10 個新文件索引標籤。舉凡可以在自己的 C# 程式碼檔案內進行的作業,都可在這個命令視窗內執行。

除此,您還能提供指令碼儲存與播放的功能,好比使用 Microsoft Word VBA 一樣。這種指令碼編寫的能力可讓您的應用程式具有無限延伸性,並且方便進階使用者快速執行例行工作。

結論

自動化支援物件模型帶來無限的可能,它能充分利用分離程式碼與 UI 邏輯,大幅提昇您應用程式的重複使用度,而且還能提供外掛程式與指令碼功能,進而達到無限延伸。

 


版权所有:UML软件工程组织