扩展Visual Studio第2部分 - 创建插件
创建一个非常实用的“切换”插件,用于在cpp/h、设计器/代码、XAML/代码隐藏等之间切换!
扩展Visual Studio
本文是“扩展Visual Studio”系列文章的一部分。
引言
这是我关于扩展Visual Studio系列文章的第二篇。在本文中,我们将探讨一种已经存在了一段时间的扩展Visual Studio的方式——插件。
我们将创建一个非常酷的插件,名为“Switch”。这个插件将允许我们在相关文件之间切换——C++头文件和源文件、WinForms代码和设计器、XAML和代码隐藏等等。
在本文中,我们将从扩展Visual Studio最简单的方式之一开始——创建代码片段。
保持最新
本文中创建的插件Switch已经非常流行——你可以在GitHub上保持代码和项目的最新状态:github.com/dwmkerr/switch。
什么是插件?
插件是一个DLL,Visual Studio将其加载到内存中以执行任务或向用户呈现功能。就开发而言,插件最有用的地方在于我们可以访问DTE2对象。DTE2是Visual Studio自动化模型中的顶级对象。它是一组接口和对象,你可以使用它们与Visual Studio进行交互。
那么我们能用DTE2做什么呢?以下是一些例子:
- 访问和调用Visual Studio提供的命令。
- 执行构建
- 遍历解决方案中的项
- 向用户界面添加控件
- ...等等...
本质上,使用Visual Studio自动化模型,如果Studio能做到,你也能做到。
简介
那么让我们来看看这个插件项目的简要介绍。我们想做什么?首先,我们可以概述我们的需求。
- Switch应该向Visual Studio 2008或Visual Studio 2010添加一个名为“Switch”的命令,我们可以通过鼠标或键盘调用它。
- Switch应该在C或CPP源文件及其头文件之间切换,反之亦然。
- Switch应该在C#窗体的代码视图和设计视图之间切换。
- Switch应该在WPF/WP7/Silverlight XAML文件的XAML和代码隐藏之间切换。
- Switch应该有一个安装程序,使部署变得简单。
我们可以从Visual Studio插件中获得前四项所需的所有功能——我们可以使用标准部署项目来处理第五项。所以,事不宜迟,让我们开始吧。顺便提一下,本文中的所有截图和代码都将基于Visual Studio 2010。源代码实际上也包含2008年的相同插件——但是代码本质上是相同的(主要类在两个项目中都作为链接添加,所以功能代码实际上是完全相同的)。
创建代码片段
启动Visual Studio 2010,选择“文件”>“新建”>“项目...”
我们将创建一个新的Addin项目,它位于“其他项目类型”的“扩展性”组中。
现在我们必须指定项目设置。我们需要注意的几点是——确保你使用的是C#或VB,确保我们选择Visual Studio作为宿主,但不是Visual Studio宏,并确保你选择“是,创建工具菜单项”。
现在我们已经创建了项目,我们的插件有了一个很好的起点。我们创建的最关键的文件是“Connect.cs”——它实际上是在插件加载时执行工作的。
Connect类
Connect类是处理插件与Visual Studio集成的类。所以让我们更详细地看一下它。
连接
构造函数,我们可以在这里执行任何可能需要的初始化。但是,一般来说,尽量将复杂的任务推迟到后面的函数中。
Exec
此函数用于实际执行插件命令。一个插件实际上可以向Visual Studio添加许多命令,此函数将用于确保我们执行正确的行为。
OnAddinsUpdate
当Visual Studio插件集更改时,将调用此函数。
OnBeginShutdown
当Visual Studio即将关闭时,将调用此函数。
OnConnection
当插件加载时,将调用此函数。正是在此函数中,我们将“Switch”命令添加到“工具”菜单中。
OnDisconnection
当插件卸载时,将调用此函数。
OnStartupComplete
当宿主应用程序(Visual Studio)完全加载时,将调用此函数。
QueryStatus
Visual Studio将调用此函数,以查看是否应显示插件、是否应启用或禁用插件等。
所以总而言之,Connect类并不是很复杂——它需要做一些工作来将Switch命令添加到适当的菜单中,但除此之外,它非常基础。它包含插件实例作为成员变量,以及`_applicationObject`——这是DTE2类,它允许我们与Visual Studio进行接口交互。
为了保持代码整洁,我们将创建一个名为“Switcher”的新类,它将执行所有切换功能。这个类将是一个单例,所以我们只需要修改Exec函数,如下所示
public void Exec(string commandName, vsCommandExecOption executeOption,
ref object varIn, ref object varOut, ref bool handled)
{
handled = false;
if(executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault)
{
if(commandName == "Switch.Connect.Switch")
{
// Use the Switcher object to perform the switch functionality
// on the application object - switching the Active Document.
Switch2010.Switcher.Instance.Switch(_applicationObject,
_applicationObject.ActiveDocument);
// We've handled the command.
handled = true;
return;
}
}
}
这让我们处于一个有利的位置。现在我们所需要做的就是创建一个单例来切换活动文档,我们就拥有了所需的核心功能。
Switcher类
现在让我们创建一个类,它将实际获取一个文档并尝试将其切换到相关文档。我们称之为`Switcher`。我将其实现为单例,它也可以是一个带有静态方法的类,这完全取决于个人偏好。我们可以做的第一件事是通过拥有一个私有的静态实例、一个用于获取实例的静态属性以及一个用于执行任何可以延迟的私有构造函数来使其“单例化”。
/// <summary>
/// The Switcher switches between related files.
/// </summary>
public class Switcher
{
/// <summary>
/// Prevents a default instance of the
/// <see cref="Switcher"/> class from being created.
/// </summary>
private Switcher()
{
// Perform any initialisation...
}
/// <summary>
/// The singleton instance.
/// </summary>
private static Switcher instance = null;
/// <summary>
/// Gets the singleton instance.
/// </summary>
public static Switcher Instance
{
get
{
// If the instance doesn't already exist, create it.
if (instance == null)
instance = new Switcher();
// Return the instance.
return instance;
}
}
这是单例的基本布局。这不是一篇关于单例模式的文章,我只是使用它,以便从现在开始不必在文章中用`static`修饰符来膨胀代码,这应该会使其更清晰。请参阅Martin Lapierre关于C#中泛型单例的优秀文章,了解一篇关于单例的非常好的文章——使用反射在C#中实现泛型单例模式。
切换器要做的第一件事是尝试确定活动文档是否具有设计器——如果存在,则尝试在代码视图和设计视图之间切换。让我们编写一些函数来完成这项工作。第一个函数将尝试确定文件是否具有设计器。
/// <summary>
/// Determines whether a document has a designer.
/// </summary>
/// <param name="path">The path of the document to check.</param>
/// <returns>True if the document has a designer.</returns>
private bool DoesDocumentHaveDesigner(string path)
{
// Create the designer path.
string designerPath = Path.Combine(Path.GetDirectoryName(path),
Path.GetFileNameWithoutExtension(path)) + ".designer.cs";
// Is there a designer document?
return File.Exists(designerPath);
}
这是一种稍微笨拙的方式来检查解决方案项是否具有设计器——是否存在一个同名文件以“_designer.cs”结尾。在Studio中确定文档是否可以拥有设计器非常困难(但不如确定窗口是否是设计器困难),所以这将以最少的努力达到目的。现在我们需要一个函数来尝试在代码视图和设计视图之间切换。
/// <summary>
/// Tries to the toggle between the code and design view.
/// </summary>
/// <param name="activeDocument">The active document.</param>
private void TryToggleCodeDesignView(Document activeDocument)
{
// If we're showing the designer, show the code view.
if (activeDocument.ActiveWindow.Caption.Contains("[Design]"))
activeDocument.ProjectItem.Open(vsViewKindCode).Activate();
else
activeDocument.ProjectItem.Open(vsViewKindDesigner).Activate();
}
这真是个权宜之计。目前(我发现的)没有编程方法可以确定活动文档是否在设计器中打开。然而,设计器窗口的标题末尾有“[设计]”字样。我们可以检查是否在设计器窗口中,然后以代码视图重新打开项目项(反之亦然)。这是我们必须注意的用于测试和进一步开发的第一点——在其他语言中,这个技巧可能不起作用!
这些函数将允许我们在代码和设计器之间切换,但如何在`cpp/h`或`xaml/xaml.cs`文件之间切换呢?为此,我们将创建一个名为`SwitchTarget`的类。`SwitchTarget`将只表示我们从哪里切换到哪里。如果文档路径以“From”结尾,我们将尝试查找一个以“To”结尾的文档并打开它。然后我们可以创建任意数量的切换目标——甚至可以潜在地将它们链接起来,以便我们可以按顺序在三个或更多文档之间切换。
这是`SwitchTarget`类——它存储了From和To,并且可以将以From结尾的路径映射到以To结尾的路径。
/// <summary>
/// A Switch Target defines what we switch from, to.
/// </summary>
public class SwitchTarget
{
/// <summary>
/// Initializes a new instance of the <see cref="SwitchTarget"/> class.
/// </summary>
public SwitchTarget()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SwitchTarget"/> class.
/// </summary>
/// <param name="from">From.</param>
/// <param name="to">To.</param>
public SwitchTarget(string from, string to)
{
From = from;
To = to;
}
/// <summary>
/// Maps the from path to the to path.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>The mapped path</returns>
public string MapPath(string path)
{
// Replace the path 'from' with the part 'to'.
if (path.Length < From.Length)
return null;
return path.Substring(0, path.Length - From.Length) + To;
}
/// <summary>
/// Gets from.
/// </summary>
public string From
{
get;
private set;
}
/// <summary>
/// Gets to.
/// </summary>
public string To
{
get;
private set;
}
}
现在我们可以将`SwitchTarget`的列表添加到`Switcher`类中,并在构造函数中设置它们。将`Switcher`的构造函数更改为以下代码,并添加集合
/// <summary>
/// The switch targets.
/// </summary>
private List<SwitchTarget> switchTargets = new List<SwitchTarget>();
/// <summary>
/// Prevents a default instance of the
/// <see cref="Switcher"/> class from being created.
/// </summary>
private Switcher()
{
// Create the switch targets.
switchTargets.Add(new SwitchTarget("c", "h"));
switchTargets.Add(new SwitchTarget("cpp", "h"));
switchTargets.Add(new SwitchTarget("h", "c"));
switchTargets.Add(new SwitchTarget("h", "cpp"));
switchTargets.Add(new SwitchTarget("xaml", "xaml.cs"));
switchTargets.Add(new SwitchTarget("xaml.cs", "xaml"));
}
现在我们有了一种方法来定义我们从什么切换到什么。有了这个,我们现在可以构建最终的`Switch`函数了
/// <summary>
/// Switches the specified active document.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="activeDocument">The active document.</param>
public void Switch(DTE2 application, Document activeDocument)
{
// Does the active document have a designer?
if (DoesDocumentHaveDesigner(activeDocument.FullName))
{
// It does, so just switch between the designer and code view.
TryToggleCodeDesignView(activeDocument);
}
// Go through each switch target - if we have
// one for this file, we can attempt to switch.
List<SwitchTarget> targets = new List<SwitchTarget>();
foreach (var target in switchTargets)
if (activeDocument.FullName.EndsWith(target.From))
targets.Add(target);
// Go through each potential target, try and open
// the document. If it opens, we're done.
foreach (var target in targets)
if(TryOpenDocument(application, target.MapPath(activeDocument.FullName)))
break;
}
就是这样!项目包含一个名为`StringExtensions.cs`的文件,其中包含一个名为“`EndsWith`”的扩展方法,以防您想知道此函数从何而来!我们还没有的唯一函数是“`TryOpenDocument`”——所以在这里。请记住,不能保证切换目标会存在,所以我们只能尝试打开它——我们不能保证。
/// <summary>
/// Try to open the document with the specified path.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="path">The path.</param>
/// <returns>True if the document was opened.</returns>
private bool TryOpenDocument(DTE2 application, string path)
{
// Go through each document in the solution.
foreach(Document document in application.Documents)
{
if(string.Compare(document.FullName, path) == 0)
{
// The document is open, we just need to activate it.
if (document.Windows.Count > 0)
{
document.Activate();
return true;
}
}
}
// The document isn't open - does it exist?
if (File.Exists(path))
{
try
{
application.Documents.Open(path, "Text", false);
return true;
}
catch
{
// We can't open the document, that's fine.
}
}
// We couldn't open the document.
return false;
}
核心功能已经完成——如果你按下F5,你可以运行Visual Studio并在相关文档之间切换。
构建安装程序
插件的安装程序非常简单——只需确保*.addin文件没有指向程序集的路径,只有程序集名称,然后将addin文件和程序集放在“我的文档/Visual Studio 2010/Addins”文件夹中。下面是MSI文件系统屏幕应如何显示的截图(主项目实际上也支持Visual Studio 2008,所以有更多文件夹!)
为命令使用自定义图标
如果您一直在关注代码并构建自己的项目,您会注意到该命令的图标是一个大大的笑脸。虽然这很活泼,但我们可能希望使用自己的图标。事实证明,这相当棘手,所以我将其留到最后。
图标是如何设置的?
查看`Connect`对象的`OnConnection`函数——关键行如下:
//Add a command to the Commands collection:
Command command = commands.AddNamedCommand2(_addInInstance,
"Switch",
"Switch",
"Switch between related files",
true, // use a visual studio icon
59, // the smiley icon.
ref contextGUIDS,
(int)vsCommandStatus.vsCommandStatusSupported+
(int)vsCommandStatus.vsCommandStatusEnabled,
(int)vsCommandStyle.vsCommandStylePictAndText,
vsCommandControlType.vsCommandControlTypeButton);
布尔参数设置为true表示以下值(59)是要使用的Visual Studio图标的索引。有数千个可供选择,本文将向您展示如何查看所有这些图标:http://msdn.microsoft.com/en-us/library/aa159658(office.11).aspx。
但是,如果我们不想使用这组图标中的一个,而是使用自己的图标呢?下面是具体操作方法。首先,向解决方案添加一个名为`Resources.resx`的新资源文件,并删除相关的设计器文件。将您的16x16图标位图添加到解决方案中。如果图标需要有透明部分,请使用颜色RGB(0, 254, 0)。下面的截图显示了带有透明特殊颜色的切换图标。
现在将图标添加到资源文件,并确保其名称为“1”。资源代码应该有这样的条目:
<data name="1" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\Switch.bmp;System.Drawing.Bitmap, System.Drawing,
Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
确保构建操作设置为“无操作”——我们将自行构建附属程序集。资源必须构建在附属程序集中,否则Visual Studio将无法识别它们。我们可以使用以下命令构建附属程序集:
"C:\Program Files\Microsoft SDKs\Windows\v7.0A\bin\resgen.exe" Resources.resx
"C:\Program Files\Microsoft SDKs\Windows\v7.0A\bin\al.exe" /t:lib
/embed:Resources.resources /culture:en-US /out:Switch.Resources.dll
现在,当我们部署Switch程序集时,我们必须确保`Switch.resources.dll`位于名为“en-US”的子文件夹中,否则Visual Studio将无法识别它!下载中包含用于执行这些命令的批处理文件。
最后要做的是将`.vscontent`和`.snippet`文件打包到一个新的*.zip文件中,并将其名称从`zip`更改为`vsi`。这将创建一个VSI安装程序——双击它,您将得到以下结果:
最后的思考
现在我们有了Switch插件,我们可以将其添加到Visual Studio UI的任何位置,并为其绑定键盘命令。在我的设置中,Switch在主菜单的右侧非常容易访问,并且我将其绑定到Ctrl+Alt+S。现在,无论我是在C++代码、XAML还是WinForms项目中,我都可以轻松地在相关文件之间切换。
该项目还有很多潜在的改进——使切换可配置化,修复在其他语言中不起作用的权宜之计等等——但像这样的基本项目是开始使用Visual Studio扩展的好方法。
希望您喜欢并觉得这篇文章有用,请关注我的博客www.dwmkerr.com,以获取我的最新文章——一如既往,欢迎提出任何建议。
更新历史
- 2013年6月11日 - 更新了文章,提供了支持新功能的2.0版安装程序。
- 2012年8月26日 - 更新了Switch安装程序下载,增加了对Visual Studio 2012的支持。