(翻譯)LearnVSXNow! #15- 創建簡單的編輯器-基礎
- 2019 年 10 月 5 日
- 筆記
在了解了菜單和命令之後,我們接下來的幾篇文章將以自定義編輯器為主題。在開發程式的時候,我們可以用文本編輯器來編寫程式程式碼,並且實際上我們可以用文本編輯器完成所有的開發工作,但我們通常不這麼做,因為在visual studio中有很多可以提高我們效率的編輯器,例如winforms編輯器和asp.net的頁面編輯器。
Visual Studio IDE允許我們創建自己的編輯器。但創建一個自定義編輯器要比創建一個Tool window複雜多了。
利用VSPackage嚮導,可以幫助我們創建一個自定義編輯器,但我不打算利用VSPackage嚮導。這是因為嚮導生成的程式碼太長了:光編輯器就有差不多有五千行的程式碼,但實際上並不需要這麼多程式碼。另外一個原因是:嚮導生成的程式碼是有bug的,經常會報出「The operation could not be completed」的異常。不過,vs2008 sdk的例子(C# Example.EditorWithToolbox) 是比較好的,大家可以從這裡入手。
這篇文章主要講述編輯器的基本結構,並且給出一個我做的例子。
Visual Studio中的編輯器
眾所周知,Visual Studio里有文本編輯器、表單編輯器等等,它們都是內部的編輯器,因為它們運行在Visual Studio的進程里。通過工具|外部工具(Tools|External Tools ),我們可以添加外部的編輯器,但它們實際上是運行在獨立的進程里的,是獨立的exe文件。
當然,也有一些外部編輯器看起來像是運行在Visual Studio裡面,例如VSTO項目中的Microsoft Word 2007或者Excel 2007。但它們實際上是作為ActiveX控制項嵌入在VS的Document Window里的,實現這種編輯器要做的工作太多了。
我們只討論運行在devenv.exe這個進程中的編輯器。這種編輯器有多種類型:
- 單視圖(Single view )編輯器。這種是最常見的編輯器。例如C#的程式碼編輯器。
- 多視圖(Multiple view )編輯器。正在編輯的數據有多個視圖。例如winform的表單設計器,它包含設計視圖和程式碼視圖,表單背後的程式碼甚至可以存放在多個文件里。我們在設計器里的一個動作會同時修改多個文件。並且,我們可以同時看到設計視圖和程式碼視圖(可以通過新建水平選項卡或者垂直選項卡)。
- 多頁簽(Multi-tabbed )編輯器。正在編輯的數據有多個視圖,但是這些視圖存在於同一個Document Window中。例如ASP.NET的頁面編輯器,它包含設計視圖和html視圖,但它和多視圖(Multiple view )編輯器最大的不同是,這些視圖是位於不同的頁簽里的,而不是不同的窗口裡。
- Visual Studio也允許我們創建和工具窗(Tool Window)綁定的編輯器。例如SQL Data Editor,當我們在Server Explorer里連接到一個資料庫之後,就可以用SQL Data Editor了。但這種編輯器不是我們這篇文章討論的內容。
編輯器的結構
編輯器的結構符合MVC結構,下圖可以幫助我們了解它的主要結構:

和工具窗(Tool Window)一樣,自定義編輯器也是從屬於VSPackage的。package可以用由vs shell提供的SVsRegisterEditors服務來註冊編輯器,實際上註冊的是一個實現了IVsEditorFactory的編輯器工廠,這個工廠負責初始化編輯器。
一個編輯器要實現很多功能,例如保存、剪切、複製、粘貼,還要負責顯示編輯器的介面,維護編輯器要編輯的文件,等等等等。實際上有兩類東西需要編輯器管理:
- 文檔視圖(Document View)。編輯器必須要有介面和用戶交互。一個編輯器通常只有一個視圖,當然也可以有兩個或者更多,例如ASP.NET的webform編輯器有一個所見即所得的設計視圖和一個html的源視圖;再比如xml schema編輯器有一個圖形視圖和xml源視圖。上圖中的Document View 1和2就分別代表兩個視圖。
- 文檔數據(Document Data)。做一個不需要編輯數據的編輯器是毫無意義的。當編輯器載入的時候,數據被載入到記憶體里,保存文檔的時候,數據被保存到文件里。當然,VS實際上並不要求數據必須保存在文件里,不過我們這篇文章只關注數據保存到文件里的情況。
文檔視圖(Document View)和文檔數據(Document Data)不一定非得用兩個類來實現,用一個類就已經足夠了。要實現編輯器,還需要實現一些介面,如下表:
介面 |
說明 |
---|---|
IVsEditorFactory |
這個介面用來創建和初始化編輯器,以及在關閉編輯器時去清理資源。該介面對於實現編輯器是必須的。 |
IOleCommandTarget |
Document view和Document data都必須識別由vs或其他package傳過來的命令。例如,document view要識別和執行「複製」和「粘貼」命令,document data要識別「載入」和「保存」命令。IOleCommandTarget介面就是負責這個的。 (在第13章的時候,我們曾經提到過Command target,在以後的文章里我們還會多次見到它。) |
IVsWindowPane |
文檔視圖實現了IVsWindowPane介面之後,就可以像vs ide中的其他窗口一樣,可以移動、停靠。 (我們第4篇文章曾經用這個介面創建了一個Tool window) |
IVsPersistDocData |
這個介面用來管理文檔數據(例如把數據載入到記憶體里,或者把它存在某個地方),它大概有10個方法,比較複雜: 文檔本身可以被持久化到任何地方。 用戶可以在VS外面修改文檔,在VS里重新載入修改後的文檔。 可以在VS里重命名文件,或者「另存為」文件。 |
IPersistFileFormat |
通常情況下,設計器對應的文檔被持久化到文件里。一個文檔可以有多種不同的文件格式。這個介面就是用來處理文件格式的。 |
- 文檔本身可以被持久化到任何地方。
- 用戶可以在VS外面修改文檔,在VS里重新載入修改後的文檔。
- 可以在VS里重命名文件,或者「另存為」文件。
IPersistFileFormat 通常情況下,設計器對應的文檔被持久化到文件里。一個文檔可以有多種不同的文件格式。這個介面就是用來處理文件格式的。
Running Document Table
編輯器擁有文檔數據和一個或多個文檔視圖。在編輯器還沒有被打開的情況下,文檔數據只是被存放在文件或資料庫(或其他地方)里,但是一旦打開了編輯器,就意味著至少有一個視圖正在處理數據,如果編輯器有多個視圖的話,還需要在多視圖之間同步數據。
例如資料庫表的設計器有Grid視圖和DDL視圖,當我們修改了其中一個視圖裡的數據之後,數據會同步到另外一個視圖。再比如Windows Forms設計器,當我們在設計視圖做了些修改之後,會把程式碼同步到程式碼視圖。
Visual Studio利用Running Document Table(RDT)來管理打開的文檔。當一個文檔的數據改變之後,它可以判斷哪些視圖和哪些文件(或其他的持久介質,例如資料庫的表、存儲過程等等)被修改了。當我們關掉一個文件或者關掉解決方案的時候,RTD就會告訴Visual Studio,從而彈出一個詢問我們是否要保存文件的對話框:

在這個對話框里的每一項都代表RDT里的一個文檔。文檔是作為一個獨立的單元來持久化的。例如,假設你打算把你的solution都存放在一個文件里的話,你就只有一個文檔;假設你的解決方案存放在兩個單獨的文件里,那麼就有兩個文檔。
RDT利用所謂的「編輯鎖」來協調已經打開的文檔。當一個文檔視圖打開的時候,這個文檔就被加到了RDT里,RDT給這個文檔加了一個「編輯鎖」,如果再打開這個文檔的另一個視圖,RDT又會給這個文檔加一個新的「編輯鎖」。所以當打開第二個視圖的時候,鎖的個數變為2。
當一個文檔對應的鎖的個數變為0的時候,VS就會提示我們去保存文檔。
那麼,RDT里到底存放了些什麼東西呢?
- 文件的路徑或uri。如果文檔是存在文件里的,RDT必須知道它的絕對路徑;如果文檔是存在資料庫里的,RDT也必須知道能夠唯一定位該文檔的地址。
- 文檔數據在記憶體里的指針。利用該指針,我們不僅可以訪問文檔的數據,也可以用來檢查數據的狀態(例如該數據是否被修改過)。
- 文檔的狀態標記。例如「Do not save this document」, 「Do not open it next time when the solution is opened」,等等。
- 鎖
- 擁有文檔的節點(hierarchy)。通常是解決方案里的一個文件的節點,但也可以是其他類型的,例如伺服器資源管理器里的資料庫節點。文檔的擁有者是很重要的,因為VS Shell不會直接保存文檔,而是讓擁有者來保存。
- 指向不可見的鎖的指針列表。add-in和package可以用不可見的方式打開文檔,RDT也會給這些打開的文檔加鎖。
編輯器的優先順序
編輯器是和文件的擴展名掛鉤的,當註冊編輯器的時候,需要指定優先順序。優先順序很重要,因為VS可以利用優先順序來給某個特定擴展名的文件選用最佳的編輯器。
當VS打開一個文件的時候,它通過該文件的擴展名找到對應的編輯器(不止一個),然後根據優先順序來決定該使用哪一個編輯器。
VS怎樣才能確保至少有一個編輯器可以打開文件呢?這個難題由Visual Studio內置的編輯器來解決:VS內置了一些編輯器(例如二進位編輯器和XML編輯器),這些編輯器和「.*」文件掛了鉤。所以,如果一個文件並沒有特定的編輯器的話,就會用這些內置的編輯器打開它們。
BlogItemEditor示例
說了這麼多,終於該看一看怎樣做一個自定義編輯器了。我的編輯器叫做BlogItemEditor,介面如下圖:

這個編輯器用來編輯簡單的部落格,有標題、分類和內容。用xml文件來存儲(我並沒有實現上傳到部落格引擎的功能)。分類是一個列表,可以有多個分類(在介面上用分號隔開),部落格內容存為CDATA元素格式:
<BlogItem xmlns=」...」> <Title>Sample Blog Item</Title> <Categories> <Category>VSX Sample</Category> <Category>Visual Studio 2008</Category> <Category>LearnVSXNow!</Category></Categories><Body> <![CDATA[ After dealing with menus and commands I take a break to show you some new topics related to custom editors. As we develop applications we use programming languages with text editors to define the code to be compiled into our product. ... Where we are? ... ]]></Body></BlogItem>
項目結構
我的編輯器用了4個核心的類,如下圖:

BlogItemEditorFactory是編輯器工廠。我把文檔視圖和文檔數據分隔到3個類裡面。BlogItemEditorPane 負責管理文檔視圖和文檔數據,BlogItemEditorControl 是編輯器的介面,BlogItemEditorData 是數據的載體。
我把創建一個簡單的編輯器的程式碼封裝了一下,放到了VsxLibrary里:
類型 |
作用 |
---|---|
SimpleEditorFactory<TEditorPane> |
編輯器工廠,負責創建編輯器 |
SimpleEditorPane<TFactory, TUIControl> |
負責管理文檔視圖和數據,TFactory是編輯器工廠,TUIControl是介面。 |
ICommonCommandSupport |
指示介面是否支援「全選」、「複製」、「粘貼」等功能。 |
IXmlPersistable |
定義用於讀取和保存XElement的方法。 |
BlogItemEditorFactory
BlogItemEditorFactory 繼承自SimpleEditorFactory<>泛型類,由於基類里已經做了創建編輯器的邏輯,所以這個子類就很簡單了:
[Guid(GuidList.guidBlogEditorFactoryString)]public sealed class BlogItemEditorFactory: SimpleEditorFactory<BlogItemEditorPane>{}
它用BlogItemEditorPane 作為編輯器的視圖和數據。
BlogItemEditorData
這個類用於處理數據:
public sealed class BlogItemEditorData : IXmlPersistable{ public BlogItemEditorData(string title, string categories, string body) { ... } public string Title { get; } public string Categories { get; } public string Body { get; } public void SaveTo(string fileName) { ... } public void ReadFrom(string fileName) { ... } // --- IXmlPersistable implementation public void SaveTo(XElement targetElement) { ... } public void ReadFrom(XElement sourceElement) { ... }}
Title、Categories和Body屬性在構造函數里初始化;SaveTo(XElement) 和 ReadFrom(XElement)是IXmlPersistable介面的方法實現;另外兩個帶有字元串參數的SaveTo和ReadFrom方法負責保存和讀取把BlogItemData。
BlogItemEditorControl
這個用戶控制項就是我們的編輯器的介面了。它實現了ICommonCommandSupport介面:
public partial class BlogItemEditorControl : UserControl, ICommonCommandSupport{ public BlogItemEditorControl() { InitializeComponent(); } // ... // --- ICommonCommandSupport implementation // ...}
ICommonCommandSupport
這個介面指示介面是否支援「全選」、「複製」、「粘貼」等功能,它的定義如下:
public interface ICommonCommandSupport{ // --- Support flags bool SupportsSelectAll { get; } bool SupportsCopy { get; } bool SupportsCut { get; } bool SupportsPaste { get; } bool SupportsRedo { get; } bool SupportsUndo { get; } // --- Command execution methods void DoSelectAll(); void DoCopy(); void DoCut(); void DoPaste(); void DoRedo(); void DoUndo();}
以Supports為前綴的屬性表示是否支援相應的命令,如果支援的話,就會調用相應的以Do為前綴的方法。
BlogItemEditorPane
我們的編輯器的主要工作是由BlogItemEditorPane 來完成的,不過,它的程式碼是很簡單的:
public sealed class BlogItemEditorPane: SimpleEditorPane<BlogItemEditorFactory, BlogItemEditorControl>{ public BlogItemEditorPane() { ... } protected override string GetFileExtension() { ... } protected override Guid GetCommandSetGuid() { ... } protected override void LoadFile(string fileName) { ... } protected override void SaveFile(string fileName) { ... }e) { ... }}
基類SimpleEditorPane接受兩個類型參數,第一個是編輯器工廠類,第二個是表示用戶介面的用戶控制項,而我們的子類只需要重寫下面幾個虛方法:
方法名 |
描述 |
---|---|
GetFileExtension |
定義我們的編輯器支援的文件擴展名 |
GetCommandSetGuid |
定義我們的編輯器可以處理的CommandSetGuid |
LoadFile |
讀取數據 |
SaveFile |
保存數據 |
現在讓我們來看一下基類SimpleEditorPane的類聲明:(遲些我會仔細說明一下這個類)
public abstract class SimpleEditorPane<TFactory, TUIControl> : WindowPane, IOleCommandTarget, IVsPersistDocData, IPersistFileFormat where TFactory: IVsEditorFactory where TUIControl: Control, ICommonCommandSupport, new(){ // ...}
SimpleEditorPane 實現了自定義編輯器需要的所有的關鍵的介面。繼承了WindowPane,我們就實現了IVsWindowPane介面;實現了IOleCommandTarget 介面,就可以處理命令;IVsPersistDocData 和IPersistFileFormat 用於持久化。TFactory是實現了IVsEditorFactory 的類,TUIControl 是實現了ICommonCommandSupport 介面的控制項。
小結
本篇文章我們開始創建一個自定義編輯器。首先我們了解了VS編輯器的基本架構。編輯器包含文檔數據和文檔視圖,一個編輯器可以有多個視圖,所有的視圖都為同一個數據工作。
VS IDE用Running Document Table來管理打開的文檔,編輯器是有優先順序的。
本篇文章只是BlogItemEditor 示例的開篇,下一篇我們將從編輯器工廠類開始。