標籤:
在 Visual Studio 2010 的時代,擴充 Visual Studio 的途徑有很多,開發人員可以選擇宏、Add-in、MEF 和 VSPackages 進行自訂的擴充。但是宏在 Visual Studio 2012 的時候被閹割了,Add-in 也在 Visual Studio 2013 裡被抹殺了,這樣的調整對於 Visual Studio 來說是好的,但是對於那些習慣了使用宏和Add-in的團隊可能就鬱悶了。
本文將一步步教你如何?對 Visual Studio 代碼編輯器的擴充,最終將實現一個可以支援如下兩個功能的擴充。
1. 自動任務注釋(支援後期擴充)
任務注釋就是 //TODO、//FIXME 之類可以被 Visual Studio 區別對待的注釋,不同的任務注釋可能需要不同的格式,比如有些需要先注釋掉方法體,有些需要在程式碼的前後加上開始、結束標記等,過去在宏還支援的時候,我們可以使用宏來實現此類操作。不同的項目組使用這些標記的方法不同,因此會有不同的要求,本工具支援自訂的擴充。
2. 跳轉到方法的頭部或尾部
這個功能看似無用,但是當一個方法有幾百行,甚至上千行的時候,如何快速跳轉到方法的開頭或結尾就比較麻煩了。
圖1 最終效果動畫示範(請點擊放大後查看)
閱讀此文,需要您對如下知識點有一定瞭解:
1. MVVM 與 WPF -> 趕緊去瞭解一下 (此篇文章躺在自己網站上,沒有來得及同步到部落格園,大家將就一下)
2. MEF -> 趕緊去瞭解一下
如果想趕緊體驗下這個擴充的話,請猛擊這裡! (僅限 Visual Studio 2012)
本文提綱
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 viewport11 _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 SelectedItem12 {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 #endregion22 23 #region ctor24 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 #endregion33 34 #region Methods35 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傳給 view12 }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 else17 {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 : UserControl2 {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 的能力了。
如何擴充 Visual Studio 編輯器