如何擴展 Visual Studio 編輯器

  在 Visual Studio 2010 的時代,擴展 Visual Studio 的途徑有不少,開發者能夠選擇宏、Add-in、MEF 和 VSPackages 進行自定義的擴展。可是宏在 Visual Studio 2012 的時候被閹割了,Add-in 也在 Visual Studio 2013 裏被抹殺了,這樣的調整對於 Visual Studio 來講是好的,可是對於那些習慣了使用宏和Add-in的團隊可能就鬱悶了。html

 

  本文將一步步教你如何實現對 Visual Studio 代碼編輯器的擴展,最終將實現一個能夠支持以下兩個功能的擴展。安全

  1. 自動任務註釋(支持後期擴展)架構

  任務註釋就是 //TODO、//FIXME 之類能夠被 Visual Studio 區別對待的註釋,不一樣的任務註釋可能須要不一樣的格式,好比有些須要先註釋掉方法體,有些須要在代碼行的先後加上開始、結束標記等,過去在宏還支持的時候,咱們可使用宏來實現此類操做。不一樣的項目組使用這些標記的方法不一樣,所以會有不一樣的要求,本工具支持自定義的擴展。app

  2. 跳轉到方法的頭部或尾部框架

  這個功能看似無用,可是當一個方法有幾百行,甚至上千行的時候,如何快速跳轉到方法的開頭或結尾就比較麻煩了。編輯器

圖1 最終效果動畫演示(請點擊放大後查看)ide

 

閱讀此文,須要您對以下知識點有必定了解:

    1. MVVM 與 WPF -> 趕忙去了解一下 (此篇文章躺在本身網站上,沒有來得及同步到博客園,你們將就一下)
    2. MEF -> 趕忙去了解一下函數

 

  若是想趕忙體驗下這個擴展的話,請猛擊這裏! (僅限 Visual Studio 2012)工具

 

  本文提綱post

  MEF 和 VSPackage

  準備工做

  各類 Editor 模板的區別

  Visual Studio 實驗環境

  Editor Viewport Adornment 原理解析

  添加控件和基礎代碼

  獲取DTE對象

  完成功能邏輯

  增長擴展點,讓註釋支持後期擴展

  Why VSPackages or MEF ?

  源代碼管理

  參考資源

 

  在進入正文前,咱們先來快速認識下這僅存的兩位英雄~

MEF 和 VSPackage

  從 Visual Studio 2013 開始,對 Visual Studio 的擴展只剩下 MEF 和 VSPackage。來來來,給你們介紹下大家本身~~ 

 

  MEF(Managed Extensibility Framework)

  這個框架最初是獨立於 .Net Framework 發佈,後來整合到了 .Net 4.0 中,並伴隨着 .Net 4.0 一塊兒發佈(包含在 System.ComponentModel.Composition.dll 程序集中)。從名字上看得出這個框架主要就是爲編寫可擴展的應用程序而生的。隨着 .Net 4.0 的推出,還有一項重大的改變就是 Visual Studio IDE中的編輯器,該部分原先和其它組件同樣都是採用 COM 方式開發,但如今卻被 WPF 技術頂替了。採用了WPF技術搭建的編輯能夠徹底支持使用 MEF 來進行擴展,這不能不說是一個很是完美的改進。惟一遺憾的是,截止2013的出現,Visual Studio 的其他部分仍然沒有從 COM 中脫離出來。

The MEF is a .NET library that lets you add and modify features of an application or component that follows the MEF programming model. The Visual Studio editor can both provide and consume MEF component parts. The MEF is contained in the .NET Framework version 4 System.ComponentModel.Composition.dll assembly.

 

--- Managed Extensibility Framework in the Editor

  所以,若是想要擴展已有的編輯器,就能夠基於MEF進行開發(好比修改針對C#代碼編輯器的高亮顏色、智能提示、括號補全等)

 

  VSPackage

  能夠說除了編輯器, Visual Studio 就是多個 VSPackage 的集合,所以使用 VSPackage 能夠完美得與 Visual Studio 進行集成,並且可以得到幾乎所有的能力。若是想開發對工具欄、菜單欄,甚至是全新的編輯器時(提供對新語言的解析、智能提示等)能夠選擇VSPackage。

 

準備工做

  童鞋們,請檢查下吃飯的傢伙準備好了嗎?

  1. 英文版的 Visual Studio,中文版的 Visual Studio 沒法看到 Editor Text Adornment 等模板。

  2. 想要擴展編輯器或整個 Visual Stuio,必須先下載安裝 Visual Studio SDK(VS2012版本,請點擊連接下載),安裝完後,就能夠在 「其它項目類型」 的模板上找到想要的模板了。

圖2 SDK安裝後的模板

  

本文全部代碼均基於 Visual Studio 2012 開發,若是您使用的是其它版本,請下載安裝適合您版本的 SDK。擴展開發的過程在各個版本間可能會有細小的差距,但不影響總體開發流程。

 

各類 Editor 模板的區別

  安裝完 SDK 後,就會擁有如上圖中所示的多種擴展模板,其中包含四種和 Editor 相關的模板,它們之間有什麼區別嗎?

 

  Editor Classifier

  能夠修改編輯器中代碼的高亮、添加一些智能的標籤(好比當咱們修改了某個變量名時,會在變量名下出現一個小短橫,當你鼠標移上去後會提示你是否要修改全部引用的地方)等,示例效果以下:

圖3 Editor Classifier 示例

 

  Editor Margin

  在編輯器的周圍添加一些WPF元素,好比當前文件是隻讀的時候,能夠在編輯器下邊沿提示文件爲只讀,示例效果以下:

圖4 在下邊沿添加了一條綠色的信息框

 

  Editor Text Adornment

  用於對編輯器中的文字進行修飾,添加一些WPF的元素,示例效果以下:

圖5 用框框包裹全部字符 a 

 

  Editor Viewport Adornment

  用於對編輯器自己進行修飾,添加一些WPF元素,示例以下:

圖6 在編輯器的右上角添加了一個矩形元素

 

 

  本工具使用 Editor Viewport Adornment 做爲模板。

 

Visual Studio 實驗環境

  對於這些擴展的測試,Visual Studio 提供了 Experimental Instance 用於實驗環境,該環境和真實的 Visual Studio 徹底同樣,只不過它和真實版本各自獨享一套配置文件,對於實驗環境的配置不會影響到真實環境。

  第一次啓動實驗環境,會進入如圖7所示的默認環境配置的界面。

圖7 默認環境配置

 

  實驗環境中的數據能夠初始化

  SDK目錄中的 Tools提供了 「Reset the Visual Studio 2012 Experimental Instance」 命令行工具,運行該工具就會初始化實驗環境。

圖10 初始化工具

Editor Viewport Adornment 原理解析

  要想理解它,必先使用它。經過模板建立好的新 Editor Viewport Adornment 項目時,已經包含了示例代碼,該示例代碼的功能就是如圖6所示在編輯區添加一個紫色的矩形框。

圖11 剛建立完的樣子

 

  在運行示例代碼前必須先修改 source.extension.vsixmanifest 文件中的 author 字段,不然運行將報錯。

圖12 補全 Author 字段

 

  實現原理

  在介紹 MEF 和 VSPackage 的時候,我說過整個 Editor 部分都是基於 MEF 思想開發的。簡單來講,Visual Studio Editor 向第三方擴展提供了產物(Export)、接受者(Import)及各類協議,第三方擴展根據對應的協議製做生產出符合的產物,而後 VS 會將第三方的產物與本身的接受者進行組合(就好像是把符合形狀的積木放入盒子中)。這樣,咱們就能在下次啓動 VS 的時候使用這個擴展了。

圖13 MEF 思想

 

  這個項目中主要的就兩個文件:TskCommentFactory.cs 和 TskComment.cs

  其中,TskCommentFactory 文件中的 PurpleBoxAdornmentFactory 就是基於 IWpfTextVIewCreationListener 這個協議的產物,也是本項目的主要入口。使用該協議能夠在編輯器視圖建立的時候加入咱們想要的操做。其中最重要的就是 TextViewCreated 方法,該方法調用了 TskComment 構造函數,從而在編輯器上增長了一塊紫色的區域。

 1     [Export(typeof(IWpfTextViewCreationListener))]
 2     [ContentType("text")]
 3     [TextViewRole(PredefinedTextViewRoles.Document)]
 4     internal sealed class PurpleBoxAdornmentFactory : IWpfTextViewCreationListener
 5     {
 6         /// <summary>
 7         /// Defines the adornment layer for the scarlet adornment. This layer is ordered 
 8         /// after the selection layer in the Z-order
 9         /// </summary>
10         [Export(typeof(AdornmentLayerDefinition))]
11         [Name("TskComment")]
12         [Order(After = PredefinedAdornmentLayers.Caret)]
13         public AdornmentLayerDefinition editorAdornmentLayer = null;
14  
15         /// <summary>
16         /// Instantiates a TskComment manager when a textView is created.
17         /// </summary>
18         /// <param name="textView">The <see cref="IWpfTextView"/> upon which the adornment should be placed</param>
19         public void TextViewCreated(IWpfTextView textView)
20         {
21             new TskComment(textView);
22         }
23     }

 

  TskComment 文件中主要就兩個方法:構造函數 和 onSizeChange 方法。在構造函數中,經過 Brush 畫出了一個紫色的矩形,併爲編輯器視圖綁定了 onSizeChange 方法。 

 1     Brush brush = new SolidColorBrush(Colors.BlueViolet);
 2     brush.Freeze();
 3     Brush penBrush = new SolidColorBrush(Colors.Red);
 4     penBrush.Freeze();
 5     Pen pen = new Pen(penBrush, 0.5);
 6     pen.Freeze();
 7  
 8     //draw a square with the created brush and pen
 9     System.Windows.Rect r = new System.Windows.Rect(0, 0, 30, 30);
10     Geometry g = new RectangleGeometry(r);
11     GeometryDrawing drawing = new GeometryDrawing(brush, pen, g);
12     drawing.Freeze();
13  
14     DrawingImage drawingImage = new DrawingImage(drawing);
15     drawingImage.Freeze();
16  
17     _image = new Image();
18     _image.Source = drawingImage;

 

  上面代碼建立了一個紫色的矩形。若是看不懂也沒關係,由於這部分代碼是要被刪除的。

  onSizeChange 中調用了 AddAdornment 這個方法,這才把紫色的矩形加到了編輯器上。

 1     public void onSizeChange()
 2     {
 3         //clear the adornment layer of previous adornments
 4         _adornmentLayer.RemoveAllAdornments();
 5  
 6         //Place the image in the top right hand corner of the Viewport
 7         Canvas.SetLeft(_image, _view.ViewportRight - 60);
 8         Canvas.SetTop(_image, _view.ViewportTop + 30);
 9  
10         //add the image to the adornment layer and make it relative to the viewport
11         _adornmentLayer.AddAdornment(AdornmentPositioningBehavior.ViewportRelative, null, null, _image, null);
12     }

 

  到如今,原理部分已經講完了,無論你信不信,你已經能夠經過修改 TskComment 中這兩個方法來實現本身的擴展了。 

 

添加控件和基礎代碼

  如何實現我要的功能呢?

  首先,咱們的工具須要一個能夠交互的界面,這沒辦法單純的用 Brush 繪製。所以須要新建一個 WPF 控件(不能是 Winform 控件,AddAdornment 只能接受 WPF 元素)。

圖14 新建 WPF 用戶控件

 

  按照 MVVM 的思想,依次添加 DelegateCommand、ViewModelBase、MainViewModel 這幾個文件,咱們的核心邏輯所有在 MainViewModel 中。

圖15 新增的文件

 

  MainWindow.xaml 中的代碼以下(省略了一些與邏輯無關的元素)。

 1 <UserControl x:Class="TskComment.MainControl"
 2              ...
 3              d:DesignHeight="41" Width="300">
 4     <UserControl.DataContext>
 5         <local:MainViewModel></local:MainViewModel>
 6     </UserControl.DataContext>
 7  
 8     <Expander>
 9         <Grid Height="41" VerticalAlignment="Top">
10             <Button Content="{}{"  Command="{Binding MoveToTopOfBlockCmd}" />
11             <Button Content="}" Command="{Binding MoveToBottomOfBlockCmd}" />
12             <Button Content="執行" Command="{Binding ExecuteCmd}" IsDefault="True"/>
13             <ComboBox IsEditable="True" ItemsSource="{Binding CMTCollection}" SelectedItem="{Binding SelectedItem}" Name="cmtCol"/>
14         </Grid>
15     </Expander>
16 </UserControl>

 

  MainViewModel 中的代碼以下(省略部分無關代碼),其中的 MoveToTopOrBottomOfBlock 和 Execute 兩個方法的代碼由於缺乏關鍵元素,暫時爲空。 

 1     #region Properties and Fields
 2      
 3     private ObservableCollection<BaseComment> _cmtCollection = new ObservableCollection<BaseComment>();
 4     public ObservableCollection<BaseComment> CMTCollection
 5     {
 6         get { return _cmtCollection; }
 7         set { _cmtCollection = value; RaisePropertyChanged("CMTCollection"); }
 8     }
 9  
10     private BaseComment selectedItem = null;
11     public BaseComment SelectedItem
12     {
13         get { return selectedItem; }
14         set { selectedItem = value; RaisePropertyChanged("SelectedItem"); }
15     }
16  
17     public DelegateCommand MoveToTopOfBlockCmd { get; set; }
18     public DelegateCommand MoveToBottomOfBlockCmd { get; set; }
19     public DelegateCommand ExecuteCmd { get; set; }
20      
21     #endregion
22  
23     #region ctor
24      
25     public MainViewModel()
26     {
27         MoveToTopOfBlockCmd = new DelegateCommand((o) => MoveToTopOrBottomOfBlock(true));
28         MoveToBottomOfBlockCmd = new DelegateCommand((o) => MoveToTopOrBottomOfBlock(false));
29         ExecuteCmd = new DelegateCommand((o) => Comment());
30     }
31      
32     #endregion
33  
34     #region Methods
35  
36     private void MoveToTopOrBottomOfBlock(bool up)
37     {
38         //... 缺乏關鍵元素
39     }
40  
41     private void Comment()
42     {
43         //... 缺乏關鍵元素
44     }
45  
46     #endregion

 

獲取DTE對象

  上一節中所缺乏的關鍵元素其實就是DTE,該對象至關於 Visual Studio 的實例,能夠經過操做該實例對編輯器中的東東進行控制(若是想要進一步瞭解,請見參考資源[1]),好比剪切、粘貼、新建行、跳轉到方法體等。因此,若是想實現開頭我所講的工具,就要依託這個對象。

 

  在宏編輯器中,能夠很輕鬆的獲取該對象,可是在這裏稍微就有點麻煩了。咱們必須藉助 Visual Studio 的其中一個產物(Export) -- SVsServiceProvider。該產物的 GetService 能夠得到這個對象。

  修改 TskCommentFactory 代碼,以下:

 1 internal sealed class TskCommentFactory : IWpfTextViewCreationListener
 2 {
 3     [Import]
 4     internal SVsServiceProvider ServiceProvider = null;   //<-- 經過這句代碼,就能夠獲取 Visual Studio 的產物
 5          
 6     //省略無關代碼
 7  
 8     public void TextViewCreated(IWpfTextView textView)
 9     {
10         DTE dte = (DTE)ServiceProvider.GetService(typeof(DTE)); //<-- 獲取DTE對象
11         new TskComment(textView, dte); //<-- 把dte傳給 view
12     }
13  
14 }

 

注:DTE 存在於 EnvDTE.dll 程序集中,SVsServiceProvider 存在於 Microsoft.VisualStudio.Shell.Immutable.10.0.dll 程序集中,須要先添加這些程序集到項目中 

 

完成功能邏輯

  既然已經獲取了關鍵元素,咱們就把 MainViewModel 中的代碼完善一下吧。

 1 private void MoveToTopOrBottomOfBlock(bool up)
 2 {
 3     if (DTE == null)
 4     {
 5         return;
 6     }
 7  
 8     CodeFunction func = Selection.ActivePoint.CodeElement[vsCMElement.vsCMElementFunction] as CodeFunction;
 9  
10     if (func != null)
11     {
12         if (up)
13         {
14             Selection.MoveToPoint(func.StartPoint);
15         }
16         else
17         {
18             Selection.MoveToPoint(func.EndPoint);
19         }
20     }
21 }
22  
23 private void Comment()
24 {
25     Selection.StartOfLine();
26     Selection.NewLine();
27     Selection.LineUp();
28     Selection.Text = "//TODO:";
29     DTE.ExecuteCommand("Edit.FormatSelection");
30 }

 

  修改 MainWindow 的代碼讓它可以接受 DTE。 

1     public partial class MainWindow : UserControl
2     {
3         public MainWindow(DTE dte)
4         {
5             InitializeComponent();
6             ((MainViewModel)this.DataContext).DTE = dte;
7         }
8     }

 

  修改 TskComment.cs 中對應的部分 

 1 private MainWindow _win;
 2  
 3 //... 省略部分代碼
 4  
 5 public TskComment(IWpfTextView view,DTE dte)
 6 {
 7     _win = new MainWindow(dte);          
 8  
 9     //...
10 }
11  
12 public void onSizeChange()
13 {
14     //...
15  
16     Canvas.SetLeft(_win, _view.ViewportRight - 310); //<-- 調整位置
17     Canvas.SetTop(_win, _view.ViewportTop + 90); //<-- 調整位置
18  
19     _adornmentLayer.AddAdornment(AdornmentPositioningBehavior.ViewportRelative, null, null, _win, null); //<-- 把 win 放到界面上
20 }

 

  哦啦,如今能夠運行了! 

圖16 動畫演示

 

增長擴展點,讓註釋支持後期擴展

  上面的代碼已經完成了,惋惜這個註釋太不人性化了,要是我想增長一個 Phase0 的註釋或者 FixMe 的註釋,還得修改代碼。所以,這裏也按照 MEF 的思想,對代碼進行升級。

  這裏只對關鍵代碼進行解釋說明,其它部分,請童鞋們查看源代碼。

 

  新建 「協議」 項目

  增長一個獨立的項目,用於存放協議接口,同時基於此接口提供一個抽象類。

 1     public interface IComment
 2     {
 3         string Title { get; }
 4         string Description { get; }
 5         void Execute(DTE dte); // 把DTE提供給第三方,這樣就能夠利用這個來操縱編輯器了
 6     }
 7      
 8     public abstract class BaseComment:IComment
 9     {
10         public abstract string Title{get;}
11  
12         public abstract string Description{get;}
13  
14         public abstract void Execute(DTE dte);
15  
16         public override string ToString()
17         {
18             return Title;
19         }
20  
21         protected TextSelection Selection(DTE dte)
22         {
23             return dte.ActiveDocument.Selection;
24         }
25  
26         protected CodeFunction Function(DTE dte)
27         {
28             return Selection(dte).ActivePoint.CodeElement[vsCMElement.vsCMElementFunction] as CodeFunction;
29         }
30     }

 

  新建 「接受者」  

  有了協議,就該在咱們的工具上增長一個接收者從而讓 MEF 幫咱們把第三方的產物和咱們的接受者組合在一塊兒

  修改 MainViewModel, 增長接收者,由於可能會有不僅一個的註釋,因此要使用 ImportMany。

    [ImportMany(typeof(BaseComment))]
    public IEnumerable<BaseComment> Comments;

 

  新建 「組合引擎」 

 1     private void Init()
 2     {
 3         //設置目錄
 4         var catalog = new AggregateCatalog();
 5         catalog.Catalogs.Add(new DirectoryCatalog("D:\\plugin\\"));
 6  
 7         _container = new CompositionContainer(catalog);
 8         try
 9         {
10             this._container.ComposeParts(this);
11         }
12         catch (CompositionException compositionException)
13         {
14             Console.WriteLine(compositionException.ToString());
15         }
16  
17         //把新的註釋綁定到集合中,讓界面上可以顯示
18         foreach (BaseComment itm in Comments)
19         {
20             CMTCollection.Add(itm);
21         }
22  
23         if (Comments != null && Comments.Count() > 0)
24         {
25             SelectedItem = CMTCollection[0];
26         }
27     }

 

  大功告成,若是您還能跟住個人節奏,那可喜可賀,您已經基本掌握了 MEF 的思想和擴展 Editor 的能力了。 

 

Why VSPackages or MEF ?

  換個問法就是 「爲何宏和Add-in不支持了?」 這答案谷歌一下確定不少,可是考慮到文章的完整性,仍是打算來作一下搬運工。

根據Microsoft所作的使用率跟蹤數據,Visual Studio中宏的使用人數不到開發人員總數的1%。這還不足以讓Microsoft放棄這個功能,Visual Studio中的宏功能維護成本太高,是另一個緣由,與其餘功能不一樣,對宏的支持,必需要隨着Visual Studio每一個新版本更新,並作大量使人疲倦的測試。理論上,用戶應該能夠在宏IDE中錄製並播聽任何功能,這給微軟的維護增長了巨大負擔。 Matt Kaufman說:宏IDE已經好幾個版本都沒有更新了。用戶把它啓動以後,很快就能看出來它像一個老版本的Visual Studio。更麻煩的是:它仍是隻支持Visual Basic。用戶不能使用C#或是其餘新的.NET語言來建立宏。

 

 --- Visual Studio 11拋棄宏

微軟已經棄用Visual Studio加載項這一基礎結構。根據MSDN上的說法,「Visual Studio 2013棄用了加載項。開發人員應該將加載項升級爲VSPackage擴展。」稍後的文檔指出,「VSPackage是Visual Studio的主要架構單位,也是部署、許可和安全的單位。Visual Studio自己的大部分就寫成了VSPackage集合。」

--- Visual Studio 2013棄用加載項

 

源代碼管理

  CodePlex: Visual Studio Editor Extension -- TaskComment

   TskComment.vsix: 您也能夠直接點擊這裏獲取此擴展,並安裝到您的 Visual Studio 當中。

 

參考資源

  [1] Visual Studio 宏的高級用法

  [2] Managed Extensibility Framework (MEF)

  [3] Extending the Editor

 

 

  本文來源於 《如何擴展 Visual Studio 編輯器》

相關文章
相關標籤/搜索