逐步解說︰使用具有編輯器擴充功能的殼層命令
您可以從 VSPackage 將功能表命令等功能新增至編輯器。 本逐步解說顯示如何叫用功能表命令,將裝飾新增至編輯器中的文字檢視。
本逐步解說示範如何將 VSPackage 與 Managed Extensibility Framework (MEF) 元件部分一起使用。 您必須使用 VSPackage 向 Visual Studio 殼層註冊功能表命令。 而且,您可以使用該命令來存取 MEF 元件部分。
使用功能表命令建立擴充功能
建立 VSPackage,將名為新增裝飾的功能表命令放在工具功能表上。
建立名為
MenuCommandTest
的 C# VSIX 專案,並新增自訂命令專案範本名稱 AddAdornment。 如需詳細資訊,請參閱使用功能表命令建立擴充功能。名為 MenuCommandTest 的解決方案開啟。 MenuCommandTestPackage 檔案具有建立功能表命令的程式碼,並將它放在工具功能表上。 此時,該命令只會顯示訊息方塊。 後續步驟將顯示如何將其變更為顯示註釋裝飾。
在 VSIX 資訊清單編輯器中,開啟 source.extension.vsixmanifest 檔案。
Assets
索引標籤應該有名為 MenuCommandTest 的 Microsoft.VisualStudio.VsPackage 資料列。儲存並關閉 source.extension.vsixmanifest 檔案。
將 MEF 擴充功能新增至命令擴充功能
在方案總管中,以滑鼠右鍵按一下方案節點,按一下新增,然後按一下新增專案。 在新增專案對話方塊的 Visual C# 底下,按一下擴充性,然後按一下 VSIX 專案。 將專案命名為
CommentAdornmentTest
。因為此專案會與強式命名的 VSPackage 組件互動,因此您必須簽署組件。 您可以重複使用已為 VSPackage 組件建立的金鑰檔案。
開啟專案屬性並選取簽署索引標籤。
選取簽署組件。
在選擇強式名稱金鑰檔案下,選取針對 MenuCommandTest 組件產生的 Key.snk 檔案。
請參閱 VSPackage 專案中的 MEF 擴充功能
因為您要將 MEF 元件新增至 VSPackage,因此您必須在資訊清單中指定這兩種資產。
注意
如需 MEF 的詳細資訊,請參閱 Managed Extensibility Framework (MEF)。
參考 VSPackage 專案中的 MEF 元件
在 MenuCommandTest 專案中,在 VSIX 資訊清單編輯器中開啟 source.extension.vsixmanifest 檔案。
在資產 索引標籤上,按一下新增。
在類型清單中,選擇 Microsoft.VisualStudio.MefComponent。
在來源清單中,選擇目前方案中的專案。
在專案清單中,選擇 CommentAdornmentTest。
儲存並關閉 source.extension.vsixmanifest 檔案。
請確保 MenuCommandTest 專案引用了 CommentAdornmentTest 專案。
在 CommentAdornmentTest 專案中,設定專案以產生組件。 在方案總管中,選取專案並查看屬性視窗中的將建置輸出複製到 OutputDirectory 屬性,並將其設定為 true。
定義註釋裝飾
註釋裝飾本身是由一個追蹤選取的文字的 ITrackingSpan,以及一些表示代表作者和文字描述的字串所組成。
定義註釋裝飾
在 CommentAdornmentTest 專案中,新增類別檔案並將其命名為
CommentAdornment
。新增下列參考:
Microsoft.VisualStudio.CoreUtility
Microsoft.VisualStudio.Text.Data
Microsoft.VisualStudio.Text.Logic
Microsoft.VisualStudio.Text.UI
Microsoft.VisualStudio.Text.UI.Wpf
System.ComponentModel.Composition
PresentationCore
PresentationFramework
WindowsBase
新增下列
using
指示詞。using Microsoft.VisualStudio.Text;
檔案應該包含名為
CommentAdornment
的類別。internal class CommentAdornment
在
CommentAdornment
類別新增三個欄位,分別用於 ITrackingSpan、作者和描述。public readonly ITrackingSpan Span; public readonly string Author; public readonly string Text;
加入可初始化欄位的建構函式。
public CommentAdornment(SnapshotSpan span, string author, string text) { this.Span = span.Snapshot.CreateTrackingSpan(span, SpanTrackingMode.EdgeExclusive); this.Author = author; this.Text = text; }
建立裝飾的視覺項目
定義裝飾的視覺項目。 在此逐步解說中,定義繼承自 Windows Presentation Foundation (WPF) 類別 Canvas 的控制項。
在 CommentAdornmentTest 專案中建立類別,並將其命名為
CommentBlock
。新增下列
using
指示詞。using Microsoft.VisualStudio.Text; using System; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes; using System.ComponentModel.Composition; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Utilities;
使
CommentBlock
類別繼承自 Canvas。internal class CommentBlock : Canvas { }
新增一些私有欄位,以定義裝飾的視覺層面。
private Geometry textGeometry; private Grid commentGrid; private static Brush brush; private static Pen solidPen; private static Pen dashPen;
新增可定義註釋裝飾的建構函式,並新增相關的文字。
public CommentBlock(double textRightEdge, double viewRightEdge, Geometry newTextGeometry, string author, string body) { if (brush == null) { brush = new SolidColorBrush(Color.FromArgb(0x20, 0x00, 0xff, 0x00)); brush.Freeze(); Brush penBrush = new SolidColorBrush(Colors.Green); penBrush.Freeze(); solidPen = new Pen(penBrush, 0.5); solidPen.Freeze(); dashPen = new Pen(penBrush, 0.5); dashPen.DashStyle = DashStyles.Dash; dashPen.Freeze(); } this.textGeometry = newTextGeometry; TextBlock tb1 = new TextBlock(); tb1.Text = author; TextBlock tb2 = new TextBlock(); tb2.Text = body; const int MarginWidth = 8; this.commentGrid = new Grid(); this.commentGrid.RowDefinitions.Add(new RowDefinition()); this.commentGrid.RowDefinitions.Add(new RowDefinition()); ColumnDefinition cEdge = new ColumnDefinition(); cEdge.Width = new GridLength(MarginWidth); ColumnDefinition cEdge2 = new ColumnDefinition(); cEdge2.Width = new GridLength(MarginWidth); this.commentGrid.ColumnDefinitions.Add(cEdge); this.commentGrid.ColumnDefinitions.Add(new ColumnDefinition()); this.commentGrid.ColumnDefinitions.Add(cEdge2); System.Windows.Shapes.Rectangle rect = new System.Windows.Shapes.Rectangle(); rect.RadiusX = 6; rect.RadiusY = 3; rect.Fill = brush; rect.Stroke = Brushes.Green; Size inf = new Size(double.PositiveInfinity, double.PositiveInfinity); tb1.Measure(inf); tb2.Measure(inf); double middleWidth = Math.Max(tb1.DesiredSize.Width, tb2.DesiredSize.Width); this.commentGrid.Width = middleWidth + 2 * MarginWidth; Grid.SetColumn(rect, 0); Grid.SetRow(rect, 0); Grid.SetRowSpan(rect, 2); Grid.SetColumnSpan(rect, 3); Grid.SetRow(tb1, 0); Grid.SetColumn(tb1, 1); Grid.SetRow(tb2, 1); Grid.SetColumn(tb2, 1); this.commentGrid.Children.Add(rect); this.commentGrid.Children.Add(tb1); this.commentGrid.Children.Add(tb2); Canvas.SetLeft(this.commentGrid, Math.Max(viewRightEdge - this.commentGrid.Width - 20.0, textRightEdge + 20.0)); Canvas.SetTop(this.commentGrid, textGeometry.GetRenderBounds(solidPen).Top); this.Children.Add(this.commentGrid); }
同時實作繪製裝飾的 OnRender 事件處理常式。
protected override void OnRender(DrawingContext dc) { base.OnRender(dc); if (this.textGeometry != null) { dc.DrawGeometry(brush, solidPen, this.textGeometry); Rect textBounds = this.textGeometry.GetRenderBounds(solidPen); Point p1 = new Point(textBounds.Right, textBounds.Bottom); Point p2 = new Point(Math.Max(Canvas.GetLeft(this.commentGrid) - 20.0, p1.X), p1.Y); Point p3 = new Point(Math.Max(Canvas.GetLeft(this.commentGrid), p1.X), (Canvas.GetTop(this.commentGrid) + p1.Y) * 0.5); dc.DrawLine(dashPen, p1, p2); dc.DrawLine(dashPen, p2, p3); } }
新增 IWpfTextViewCreationListener
IWpfTextViewCreationListener 是可用來接聽檢視建立事件的 MEF 元件部分。
新增類別檔案至 CommentAdornmentTest 專案,並將其命名為
Connector
。新增下列
using
指示詞。using System.ComponentModel.Composition; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Utilities;
宣告實作 IWpfTextViewCreationListener 的類別,並以「text」的ContentTypeAttribute 和 Document 的 TextViewRoleAttribute 將其匯出。 內容類型屬性會指定元件套用的內容種類。 文字類型是所有非二進位檔案類型的基底類型。 因此,幾乎所有建立的文字檢視都會是此類型。 文字檢視角色屬性會指定元件所套用的文字檢視類型。 文件文字檢視角色通常會顯示由行組成的文字,並儲存在檔案中。
實作 TextViewCreated 方法,使其呼叫
CommentAdornmentManager
的靜態Create()
事件。public void TextViewCreated(IWpfTextView textView) { CommentAdornmentManager.Create(textView); }
新增可用來執行命令的方法。
static public void Execute(IWpfTextViewHost host) { IWpfTextView view = host.TextView; //Add a comment on the selected text. if (!view.Selection.IsEmpty) { //Get the provider for the comment adornments in the property bag of the view. CommentAdornmentProvider provider = view.Properties.GetProperty<CommentAdornmentProvider>(typeof(CommentAdornmentProvider)); //Add some arbitrary author and comment text. string author = System.Security.Principal.WindowsIdentity.GetCurrent().Name; string comment = "Four score...."; //Add the comment adornment using the provider. provider.Add(view.Selection.SelectedSpans[0], author, comment); } }
定義裝飾層
若要新增裝飾,您必須定義裝飾層。
定義裝飾層
在
Connector
類別中 ,宣告類型 AdornmentLayerDefinition 的公用欄位,並以 指定裝飾層唯一名稱的 NameAttribute 將其匯出 ,以及定義這個裝飾層與其他文字檢視層 (文字、插入號和選取項目) 的 Z 順序關聯性的 OrderAttribute。[Export(typeof(AdornmentLayerDefinition))] [Name("CommentAdornmentLayer")] [Order(After = PredefinedAdornmentLayers.Selection, Before = PredefinedAdornmentLayers.Text)] public AdornmentLayerDefinition commentLayerDefinition;
提供註釋裝飾
定義裝飾時,也要實作註釋裝飾提供者和註釋裝飾管理員。 註釋裝飾提供者會保留註釋裝飾清單、接聽基礎文字緩衝區上的 Changed 事件,並在刪除基礎文字時刪除註釋裝飾。
在 CommentAdornmentTest 專案中新增類別檔案,並將其命名為
CommentAdornmentProvider
。新增下列
using
指示詞。using System; using System.Collections.Generic; using System.Collections.ObjectModel; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor;
新增名為
CommentAdornmentProvider
的類別。internal class CommentAdornmentProvider { }
新增文字緩衝區的私有欄位,以及與緩衝區相關的註釋裝飾清單。
private ITextBuffer buffer; private IList<CommentAdornment> comments = new List<CommentAdornment>();
新增
CommentAdornmentProvider
的建構函式。 此建構函式應該具有私人存取權限,因為提供者是由Create()
方法具現化。 建構函式會將OnBufferChanged
事件處理常式新增至 Changed 事件。private CommentAdornmentProvider(ITextBuffer buffer) { this.buffer = buffer; //listen to the Changed event so we can react to deletions. this.buffer.Changed += OnBufferChanged; }
加入
Create()
方法。public static CommentAdornmentProvider Create(IWpfTextView view) { return view.Properties.GetOrCreateSingletonProperty<CommentAdornmentProvider>(delegate { return new CommentAdornmentProvider(view.TextBuffer); }); }
加入
Detach()
方法。public void Detach() { if (this.buffer != null) { //remove the Changed listener this.buffer.Changed -= OnBufferChanged; this.buffer = null; } }
新增
OnBufferChanged
事件處理常式。private void OnBufferChanged(object sender, TextContentChangedEventArgs e) { //Make a list of all comments that have a span of at least one character after applying the change. There is no need to raise a changed event for the deleted adornments. The adornments are deleted only if a text change would cause the view to reformat the line and discard the adornments. IList<CommentAdornment> keptComments = new List<CommentAdornment>(this.comments.Count); foreach (CommentAdornment comment in this.comments) { Span span = comment.Span.GetSpan(e.After); //if a comment does not span at least one character, its text was deleted. if (span.Length != 0) { keptComments.Add(comment); } } this.comments = keptComments; }
新增
CommentsChanged
事件的宣告。public event EventHandler<CommentsChangedEventArgs> CommentsChanged;
建立
Add()
方法以新增裝飾。public void Add(SnapshotSpan span, string author, string text) { if (span.Length == 0) throw new ArgumentOutOfRangeException("span"); if (author == null) throw new ArgumentNullException("author"); if (text == null) throw new ArgumentNullException("text"); //Create a comment adornment given the span, author and text. CommentAdornment comment = new CommentAdornment(span, author, text); //Add it to the list of comments. this.comments.Add(comment); //Raise the changed event. EventHandler<CommentsChangedEventArgs> commentsChanged = this.CommentsChanged; if (commentsChanged != null) commentsChanged(this, new CommentsChangedEventArgs(comment, null)); }
新增
RemoveComments()
方法。public void RemoveComments(SnapshotSpan span) { EventHandler<CommentsChangedEventArgs> commentsChanged = this.CommentsChanged; //Get a list of all the comments that are being kept IList<CommentAdornment> keptComments = new List<CommentAdornment>(this.comments.Count); foreach (CommentAdornment comment in this.comments) { //find out if the given span overlaps with the comment text span. If two spans are adjacent, they do not overlap. To consider adjacent spans, use IntersectsWith. if (comment.Span.GetSpan(span.Snapshot).OverlapsWith(span)) { //Raise the change event to delete this comment. if (commentsChanged != null) commentsChanged(this, new CommentsChangedEventArgs(null, comment)); } else keptComments.Add(comment); } this.comments = keptComments; }
新增會傳回指定快照集範圍中的所有命令的
GetComments()
方法。public Collection<CommentAdornment> GetComments(SnapshotSpan span) { IList<CommentAdornment> overlappingComments = new List<CommentAdornment>(); foreach (CommentAdornment comment in this.comments) { if (comment.Span.GetSpan(span.Snapshot).OverlapsWith(span)) overlappingComments.Add(comment); } return new Collection<CommentAdornment>(overlappingComments); }
新增名為
CommentsChangedEventArgs
的類別,如下所示。internal class CommentsChangedEventArgs : EventArgs { public readonly CommentAdornment CommentAdded; public readonly CommentAdornment CommentRemoved; public CommentsChangedEventArgs(CommentAdornment added, CommentAdornment removed) { this.CommentAdded = added; this.CommentRemoved = removed; } }
管理註釋裝飾
註釋裝飾管理員會建立裝飾,並將它新增至裝飾層。 它會接聽 LayoutChanged 和 Closed 事件,以便移動或刪除裝飾。 它也會接聽新增或移除註釋時由註釋裝飾提供者所引發 CommentsChanged
的事件。
新增類別檔案至 CommentAdornmentTest 專案,並將其命名為
CommentAdornmentManager
。新增下列
using
指示詞。using System; using System.Collections.Generic; using System.Windows.Media; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Formatting;
新增名為
CommentAdornmentManager
的類別。internal class CommentAdornmentManager { }
新增一些私用欄位。
private readonly IWpfTextView view; private readonly IAdornmentLayer layer; private readonly CommentAdornmentProvider provider;
新增將管理員訂閱至 LayoutChanged 和 Closed 事件,以及訂閱至
CommentsChanged
事件的建構函式。 建構函式是私用的,因為管理員是由靜態Create()
方法具現化。private CommentAdornmentManager(IWpfTextView view) { this.view = view; this.view.LayoutChanged += OnLayoutChanged; this.view.Closed += OnClosed; this.layer = view.GetAdornmentLayer("CommentAdornmentLayer"); this.provider = CommentAdornmentProvider.Create(view); this.provider.CommentsChanged += OnCommentsChanged; }
新增取得提供者的
Create()
方法,或視需要建立方法。public static CommentAdornmentManager Create(IWpfTextView view) { return view.Properties.GetOrCreateSingletonProperty<CommentAdornmentManager>(delegate { return new CommentAdornmentManager(view); }); }
新增
CommentsChanged
處理常式。private void OnCommentsChanged(object sender, CommentsChangedEventArgs e) { //Remove the comment (when the adornment was added, the comment adornment was used as the tag). if (e.CommentRemoved != null) this.layer.RemoveAdornmentsByTag(e.CommentRemoved); //Draw the newly added comment (this will appear immediately: the view does not need to do a layout). if (e.CommentAdded != null) this.DrawComment(e.CommentAdded); }
新增 Closed 處理常式。
private void OnClosed(object sender, EventArgs e) { this.provider.Detach(); this.view.LayoutChanged -= OnLayoutChanged; this.view.Closed -= OnClosed; }
新增 LayoutChanged 處理常式。
private void OnLayoutChanged(object sender, TextViewLayoutChangedEventArgs e) { //Get all of the comments that intersect any of the new or reformatted lines of text. List<CommentAdornment> newComments = new List<CommentAdornment>(); //The event args contain a list of modified lines and a NormalizedSpanCollection of the spans of the modified lines. //Use the latter to find the comments that intersect the new or reformatted lines of text. foreach (Span span in e.NewOrReformattedSpans) { newComments.AddRange(this.provider.GetComments(new SnapshotSpan(this.view.TextSnapshot, span))); } //It is possible to get duplicates in this list if a comment spanned 3 lines, and the first and last lines were modified but the middle line was not. //Sort the list and skip duplicates. newComments.Sort(delegate(CommentAdornment a, CommentAdornment b) { return a.GetHashCode().CompareTo(b.GetHashCode()); }); CommentAdornment lastComment = null; foreach (CommentAdornment comment in newComments) { if (comment != lastComment) { lastComment = comment; this.DrawComment(comment); } } }
新增繪製註釋的私人方法。
private void DrawComment(CommentAdornment comment) { SnapshotSpan span = comment.Span.GetSpan(this.view.TextSnapshot); Geometry g = this.view.TextViewLines.GetMarkerGeometry(span); if (g != null) { //Find the rightmost coordinate of all the lines that intersect the adornment. double maxRight = 0.0; foreach (ITextViewLine line in this.view.TextViewLines.GetTextViewLinesIntersectingSpan(span)) maxRight = Math.Max(maxRight, line.Right); //Create the visualization. CommentBlock block = new CommentBlock(maxRight, this.view.ViewportRight, g, comment.Author, comment.Text); //Add it to the layer. this.layer.AddAdornment(span, comment, block); } }
使用功能表命令來新增註釋裝飾
您可以使用功能表命令,藉由實作 VSPackage 的 MenuItemCallback
方法來建立註釋裝飾。
新增下列參考至 MenuCommandTest 專案:
Microsoft.VisualStudio.TextManager.Interop
Microsoft.VisualStudio.Editor
Microsoft.VisualStudio.Text.UI.Wpf
開啟 AddAdornment.cs 檔案,並新增下列
using
指示詞。using Microsoft.VisualStudio.TextManager.Interop; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Editor; using CommentAdornmentTest;
刪除
Execute()
方法並新增下列命令處理常式。private async void AddAdornmentHandler(object sender, EventArgs e) { }
新增程式碼以取得使用中檢視。 您必須取得 Visual Studio 殼層的
SVsTextManager
才能取得使用中的IVsTextView
。private async void AddAdornmentHandler(object sender, EventArgs e) { IVsTextManager txtMgr = (IVsTextManager) await ServiceProvider.GetServiceAsync(typeof(SVsTextManager)); IVsTextView vTextView = null; int mustHaveFocus = 1; txtMgr.GetActiveView(mustHaveFocus, null, out vTextView); }
如果這個文字檢視是編輯器文字檢視的執行個體,您可以將它轉換成 IVsUserData 介面,然後取得 IWpfTextViewHost 及其相關聯的 IWpfTextView。 使用 IWpfTextViewHost 呼叫
Connector.Execute()
方法,這個方法會取得註釋裝飾提供者,並新增裝飾。 命令處理常式現在看起來應該像此程式碼:private async void AddAdornmentHandler(object sender, EventArgs e) { IVsTextManager txtMgr = (IVsTextManager) await ServiceProvider.GetServiceAsync(typeof(SVsTextManager)); IVsTextView vTextView = null; int mustHaveFocus = 1; txtMgr.GetActiveView(mustHaveFocus, null, out vTextView); IVsUserData userData = vTextView as IVsUserData; if (userData == null) { Console.WriteLine("No text view is currently open"); return; } IWpfTextViewHost viewHost; object holder; Guid guidViewHost = DefGuidList.guidIWpfTextViewHost; userData.GetData(ref guidViewHost, out holder); viewHost = (IWpfTextViewHost)holder; Connector.Execute(viewHost); }
將 AddAdornmentHandler 方法設定為 AddAdornment 建構函式中 AddAdornment 命令的處理常式。
private AddAdornment(AsyncPackage package, OleMenuCommandService commandService) { this.package = package ?? throw new ArgumentNullException(nameof(package)); commandService = commandService ?? throw new ArgumentNullException(nameof(commandService)); var menuCommandID = new CommandID(CommandSet, CommandId); var menuItem = new MenuCommand(this.AddAdornmentHandler, menuCommandID); commandService.AddCommand(menuItem); }
建置並測試程式碼
組建方案並開始偵錯。 應該會出現實驗執行個體。
建立文字檔 輸入一些文字,然後選取它。
在工具功能表上,按一下叫用新增裝飾。 提示氣球應該會顯示在文字視窗右側,而且應該包含類似下列文字的文字。
YourUserName
Fourscore...