VS2010 Extension实践
最近VS2010 Extension在Visual Studio Blog(http://blogs.msdn.com/visualstudio/) 上提得很频繁,于是也想翻来文档研究研究,结果居然找了半天, 居然没有一丁点完整介绍这一块的,于是,只好自己找着VS IDE上的模板提供的内容和Visual Studio Blog上的讲解,一边Reflector参演,一边涂鸦一些代码,准备实弹演练一下, 但是觉得这个模板建出来的Extension也太简单了,刚好看到AxTool(http://www.axtools.com/products-vs2010-extensions.php) 有一个代码编辑器扩展, 也是VS Extension的,于是就照着这个,自己一步一步做一下。
基础篇
首先,要想建立VS Extension工程,你需要安装VS2010 SDK,目前是Beta2版本,你可以到这里可以下载:http://go.microsoft.com/fwlink/?LinkID=165597), 这里我是通过Editor Text Adornment模板创建的工程,嗯,我就不详细写如何通过模板创建自己Extension工程了, 如果你不熟悉这里,可以参考Quan To的这篇帖子——Building and publishing an extension for Visual Studio 2010 。
建好工程以后,会自动生成 TextViewCreationListener
,这里实现了 IWpfTextViewCreationListener
接口,并通过MEF导出 IWpfTextViewCreationListener
对象:
1
2
3
4
5
6
7
8
9
10
[TextViewRole("DOCUMENT")]
[Export(typeof(IWpfTextViewCreationListener))]
[ContentType("text")]
internal sealed class PETextViewCreationListener : IWpfTextViewCreationListener
{
void IWpfTextViewCreationListener.TextViewCreated(IWpfTextView textView)
{
//...
}
}
这样VS就会在合适的时候调用 IWpfTextViewCreationListener.TextViewCreated
方法来通知文字编辑的状态改变。
为了实现浮动一个自己的工具栏,这里还需要导出一个 AdornmentLayerDefinition
,
并通过 Order Attribute
来定制这个Adornment层的显示位置和次序:
1
2
3
4
5
6
7
8
[Name("QuickToolbarAdornmentLayer")]
[Order(After = "Text")]
[Export(typeof(AdornmentLayerDefinition))]
public AdornmentLayerDefinition QuickToolbarLayerDefinition
{
get;
set;
}
这里的 Name Attribute
很重要,以后的代码中要获取我们的 AdornmentLayer
就得靠它了:
1
adornmentLayer = this._textView.GetAdornmentLayer("QuickToolbarAdornmentLayer");
扯得远了,回到 IWpfTextViewCreationListener.TextViewCreated
,通过这里,可以得到一个 IWpfTextView
,
这是所有操作的目标和展现,另外,还需要挂他的 Closed
、LayoutChanged
、MouseHovered
、SelectionChanged
等事件,以响应用户行为。
由于我们要通过工具栏操作代码,所以需要通过MEF导入 IEditorOperationsFactoryService
:
1
2
3
4
5
6
[Import]
internal IEditorOperationsFactoryService EditorOperationsFactoryService
{
get;
set;
}
这样就可以在 IWpfTextViewCreationListener.TextViewCreated
中通过 IEditorOperationsFactoryService.GetEditorOperations(ITextView)
来获得 IEditorOperations
,有了它,就可以方便快捷的编辑代码了。
接下来要实现工具栏的界面,这个就不多说了,建一个 UserControl
,里面放上 ToolBar
就搞定了。
那么何时何地显示这个 ToolBar
呢?这就要依赖 IWpfTextView
的 SelectionChanged
事件了,上面提到会挂这个事件就是为这里用的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
private void MayBeAdornmentShowCondition()
{
if (!this._textView.Selection.IsEmpty)
{
SnapshotPoint startPos = this._textView.Selection.Start.Position;
SnapshotPoint endPos = this._textView.Selection.End.Position;
IWpfTextViewLine textViewLineContainingBufferPosition = this._textView.GetTextViewLineContainingBufferPosition(startPos);
TextBounds characterBounds = textViewLineContainingBufferPosition.GetCharacterBounds(startPos);
TextBounds bounds2 = this._textView.GetTextViewLineContainingBufferPosition(endPos).GetCharacterBounds(endPos);
if (this._fromMouseHover)
{
this._mustHaveAdornmentDisplayed = true;
}
else
{
PELeftButtonMouseProcessor property = null;
try
{
property = this._textView.Properties.GetProperty<PELeftButtonMouseProcessor>(typeof(PELeftButtonMouseProcessor));
}
catch
{
}
this._mustHaveAdornmentDisplayed = (property != null)
&& (property.IsLeftButtonDown
|| ((DateTime.Now - property.LastLeftButtonDownTime).TotalMilliseconds < 400.0));
}
if (this._mustHaveAdornmentDisplayed)
{
TextBounds selectionBounds = !this._textView.Selection.IsReversed ? bounds2 : characterBounds;
int offset = 7;
double top = selectionBounds.Top + (!this._textView.Selection.IsReversed ? (offset + textViewLineContainingBufferPosition.Height) : (-offset - this._adornmentUI.ActualHeight));
if (top < 0.0)
{
top = 0.0;
}
double left = characterBounds.Left + ((bounds2.Left - characterBounds.Left) / 2.0);
if ((left + this._adornmentUI.ActualWidth) > this._textView.ViewportWidth)
{
left = this._textView.ViewportWidth - this._adornmentUI.ActualWidth;
}
Canvas.SetTop(this._adornmentUI, top);
Canvas.SetLeft(this._adornmentUI, left);
long chars = 0L;
try
{
chars = this._textView.Selection.SelectedSpans[0].Span.Length;
}
catch
{
}
this._adornmentUI.SetStatus(chars);
this.RenderSelectionPopup();
}
}
else
{
this._mustHaveAdornmentDisplayed = false;
this._adornmentLayer.RemoveAdornmentsByTag(this._adornmentTag);
}
}
private void RenderSelectionPopup()
{
if (this._mustHaveAdornmentDisplayed)
{
IAdornmentLayerElement element = null;
try
{
element = this._adornmentLayer.Elements.First<IAdornmentLayerElement>(
(IAdornmentLayerElement ile) => ile.Tag.ToString() == this._adornmentTag);
}
catch (InvalidOperationException)
{
}
if (element == null)
{
this._adornmentLayer.AddAdornment(this._textView.Selection.SelectedSpans[0], this._adornmentTag, this._adornmentUI);
}
this._timer.Stop();
this._timer.Start();
}
}
private void selection_SelectionChanged(object sender, EventArgs e)
{
this._fromMouseHover = false;
this.MayBeAdornmentShowCondition();
}
然后要注意的是IWpfTextView
的Closed
事件处理要记得取消所有挂这个事件等等收尾工作。
接下来编译工程,打包VSIX就完成了,目前实现的主要Feature:
- 当在代码编辑器中选择一段文字,并将鼠标移到文字区域时,QuickToolbar会以半透明的方式“浮”文字的旁边。
- 当鼠标移到QuickToolbar区域,QuickToolbar会变成不透明,其上的按钮会响应鼠标动作。
- 目前支持的操作有:
- 剪切(Cut)
- 复制(Copy)
- 粘贴(Paste)
- 删除(Delete)
- 减小缩进(Decrease Indent)
- 增加缩进(Increase Indent)
- 注释代码(Comment)
- 取消注释(Uncomment)
调用VS内部命令
在基础篇里,主要展示了如何使用MEF扩展VS2010,来扩展编辑控制和展现自己的UI; 在实现QuickToolbar的时候,发现MEF仅仅提供了很基本的编辑控制,如果需要高级的操作,比如注释选择的代码,就捉襟见肘,很是麻烦。
本篇我将展示如何深入挖掘VS2010 Extension,使它成为锋利的军刀,而不是绣花枕头。 鉴于此,这里就从上面提到了的Feature——注释和取消注释选择的代码来剖析,希望可以为大家拓宽思路,更好的利用VS2010。
首先回顾一下基础篇中的实现,当时是基于 TextViewLine
做注释代码的,这里有两个潜在问题:
- 其一,TextViewLine,顾名思义,是“可视区域”的行,所以如果选择超出可视区域,超出的部分就没有注释掉;
- 其二,当选择的结束位置在行的结尾时,无法实现IDE注释代码后保持Caret在选择结尾而不跳到下一行的行为,当尝试自己重新选择并移动Caret就会收到
ITextSpanshot
无效的异常。
上面提到了VS2010 Extension对编辑器的编辑行为的控制能力仅仅提供了通用的,比如Cut/Copy/Paste等等。
而其他的诸如注释/取消注释代码,添加、删除、导航到Bookmark等程序员常用功能没有暴露出来,具体可以参考接口 IEditorOperations
( http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.text.operations.ieditoroperations_methods%28VS.100%29.aspx ),这里列举的所有Member表达了其所支持的编辑操作。
总之,这条路只有这么几个目的地。
那么,还有其他方法吗?貌似走到了死胡同了,但是当我们使用IDE时候,却是可以很容易的通过Edit菜单找到所有的功能的,问题是,它们要怎样才能为我所用呢?
我首先想到的是在VSSDK中找找,结果一个名字看起来很顺眼的接口撞到眼里,它就是接口 IVsUIShell
( http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.shell.interop.ivsuishell%28VS.100%29.aspx ),MSDN这么说的:
This interface provides access to basic windowing functionality, including access to and creation of tool windows and document windows. provided by the environment.
也就是说这是一个由IDE提供的全局的Service,可以创建、访问工具窗口和编辑窗口。
浏览一下这个所有Member,发现了一个叫 IVsUIShell.PostExecCommand(/*...*/)
( http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.shell.interop.ivsuishell.postexeccommand%28VS.100%29.aspx )的方法,
MSDN描述说通过它可以异步执行Command,那么,只要找到注释代码的Command,在通过这个接口就可以实现VS IDE一样的注释代码的Feature了。
酷毙了,就是它,当怎么得到它呢?现在请留心MSDN上的解释,就是上面我使用红色粗体表示出来的部分——这个由IDE提供的全局的Service,那么可以通过 Package.GetGlobalService(/*...*/)
(http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.shell.package.getglobalservice%28VS.100%29.aspx )来获取:
1
IVsUIShell shell = Package.GetGlobalService(typeof(IVsUIShell)) as IVsUIShell;
接下来是找到自己需要Command,然后PostExecCommand就搞定了;
而VS提供的Command有两部分组成:Guid和CommandID,这个大部分都在类 VSConstants
( http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.vsconstants%28VS.100%29.aspx )里面,
以注释代码为例,其Guid是:VsConstants.VSStd2k
,而 CommandID
是 VSConstants.VSStd2kCmdID.COMMENTBLOCK
。下面是我包装的注释和取消注释的代码片段:
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void ProcessComments(bool comment)
{
IVsUIShell shell = Package.GetGlobalService(typeof(IVsUIShell)) as IVsUIShell;
if (shell != null)
{
Guid std2k = VSConstants.VSStd2K;
uint cmdId = comment ?
(uint)VSConstants.VSStd2KCmdID.COMMENT_BLOCK :
(uint)VSConstants.VSStd2KCmdID.UNCOMMENT_BLOCK;
object arg = null;
shell.PostExecCommand(ref std2k, cmdId, 0, ref arg);
}
}
至此,我们通过VSSDK提供的能力,顺利的挖掘出VS2010 Extension的部分宝藏,你是不是也有点心动,要自己去挖掘一点呢?
实现自定义配置
在上面的两篇曾提到通过VSSDK(MSDN也叫VSX)来拓宽思路,实现一些MEF Extension所不能做到的功能,
比如获取 IVsUIShell
服务来执行 Command
等等,这里我给各位看官展示如何通过VSX提供自定义配置到IDE里面。
首先创建一个Package工程,找到里面的 XX_Package.cs
,要提供自定义配置到IDE,需要在这里通过 ProviderProfile
和 ProviderOptionPage
告诉 Package
两个重要信息:
此Package
有配置信息(Profiler
)以及对应该配置信息的界面,这里我从我的GotoDef extension工程里截了一张图:
其中ProvideProfile
告诉Package
提供的Profiler
的相关信息:
关联的提供该Profiler
的类型、分类名称、页面名称、资源ID等等,
VS在需要时会把保存的信息(默认在注册表里)读取并反序列化成关联的类型的对象,
在关闭Option对话框或者确认应用配置时,会把配置信息对象序列化保存(默认在注册表)。
另一个ProvideOptionPage
来指定配置信息对象和界面,它是从DialogPage
派生, 需要注意的是需要为它提供Guid
和ClassInterface
类型;
默认情况下,显示该配置对象使用PropertyGrid
,当然,可以通过override Window属性来自定义自己的UI, 比如GoToDef中的配置UI,如下图:
完成以后在VS Extension工程中引用这个Package
并添加到VSIX输出中,这样就可以使用配置了:
至此,为VS Extension提供自定义配置的工作就完成了,具体效果如下