用于更新项目属性的Visual Studio扩展
Visual Studio 2012 插件,可批量更新解决方案中所有项目的属性。
引言
这是一个简单的 Visual Studio 2012 插件 (.vsix),它将批量更新解决方案中所有项目的属性,并且是编写 Visual Studio 扩展的有用入门。
目录
- 背景
- 使用演示
- Using the Code
- UpdateProjectProperties.vsct
- UpdateProjectPropertiesPackage
- fUpdateSettings
- 关注点
- 历史
背景
当你对一个解决方案进行更改时,是否曾感到沮丧,不得不一个接一个地修改每个项目的版本号,或者你为客户创建了一个应用程序,却不得不一个接一个地修改每个项目的公司名称和版权信息?我当然有这种感受,这简直是枯燥乏味的浪费时间,在点击“生成”之前不必要地耗费了时间。
使用演示
要安装演示,请解压演示文件并在C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\IDE文件夹中打开命令提示符。现在键入vsixinstaller "vsix 文件完整路径"。这将把扩展安装到 Visual Studio 中(如果在安装时 Visual Studio 处于打开状态,您可能需要重新启动它才能使用该扩展)。
此扩展在解决方案资源管理器的工具栏中创建一个按钮
当您点击该按钮时,会弹出一个简单的屏幕,您可以在其中输入属性值并进行设置:只需输入一个值,然后点击该值旁边的“设置”即可将其应用于解决方案中的所有项目。
使用代码
在为 Visual Studio 创建扩展之前,您必须安装 Visual Studio SDK。这将会在创建新项目时,在“扩展性”下添加一个“Visual Studio 包”模板。当您运行 Visual Studio 包向导时,Visual Studio 会创建一大堆文件。我们真正感兴趣的只有两个:UpdateProjectProperties.vsct文件和UpdateProjectPropertiesPackage.cs文件。除了自定义这两个文件,我所需要做的就是添加一个表单 (fUpdateSettings.cs) 来提供用户界面并更新选定的属性。
UpdateProjectProperties.vsct
命令表文件 (.vsct) 描述了 VSPackage 中包含的命令集。在我们的例子中,我们需要在解决方案资源管理器的工具栏中添加一个带图标的按钮。
首先,我们为按钮创建一个菜单组,并告诉 Visual Studio 该组应添加到解决方案资源管理器的工具栏中。
<group guid="guidUpdateProjectPropertiesCmdSet" id="SolutionToolbarGroup" priority="0xF000">
<parent guid="guidSHLMainMenu" id="IDM_VS_TOOL_PROJWIN">
</parent></group>
Parent 元素告诉 VS 将组添加到何处,IDM_VS_TOOL_PROJWIN
是解决方案资源管理器工具栏的常量。
接下来我们为按钮定义一个图像
<!--The bitmaps section is used to define the bitmaps that are used for the commands.--> <Bitmaps> <!-- The bitmap id is defined in a way that is a little bit different from the others: the declaration starts with a guid for the bitmap strip, then there is the resource id of the bitmap strip containing the bitmaps and then there are the numeric ids of the elements used inside a button definition. An important aspect of this declaration is that the element id must be the actual index (1-based) of the bitmap inside the bitmap strip. --> <Bitmap guid="guidImages" href="Resources\drawing4.png" usedList="bmpPic1"/> </Bitmaps>
href
指向项目中作为嵌入资源包含的图像文件,usedList
指向文件中 <Symbols>
部分中定义的符号。
接下来我们定义实际的按钮
<Buttons>
<!--To define a menu group you have to specify its ID, the parent menu and its display priority.
The command is visible and enabled by default. If you need to change the visibility, status, etc, you can use
the CommandFlag node.
You can add more than one CommandFlag node e.g.:
<CommandFlag>DefaultInvisible</CommandFlag>
<CommandFlag>DynamicVisibility</CommandFlag>
If you do not want an image next to your command, remove the Icon node /> -->
<Button guid="guidUpdateProjectPropertiesCmdSet" id="cmdUpdateProjectProperties" priority="0x0100" type="Button">
<Parent guid="guidUpdateProjectPropertiesCmdSet" id="SolutionToolbarGroup" />
<Icon guid="guidImages" id="bmpPic1" />
<Strings>
<ButtonText>Update Project Properties</ButtonText>
</Strings>
</Button>
</Buttons>
我们定义按钮并告诉 Visual Studio 它的父级是我们之前定义的菜单组,要用于按钮的图像以及按钮的文本应该是什么。
最后,我们在文件 的 <Symbols>
部分中将其组合在一起,该部分定义了 vsct 文件中其他元素使用的 Guids 和 ID
<Symbols>
<!-- This is the package guid. -->
<GuidSymbol name="guidUpdateProjectPropertiesPkg" value="{d5729cc0-a507-4be2-8cb2-48001eec2e43}" />
<!-- This is the guid used to group the menu commands together -->
<GuidSymbol name="guidUpdateProjectPropertiesCmdSet" value="{76f8f297-6941-406e-aa6f-6e4eb84e6881}">
<IDSymbol name="SolutionToolbarGroup" value="0x0190" />
<IDSymbol name="cmdUpdateProjectProperties" value="0x0100" />
</GuidSymbol>
<GuidSymbol name="guidImages" value="{bcfebf3e-9279-42ee-91ef-47127a7990f7}" >
<IDSymbol name="bmpPic1" value="1" />
</GuidSymbol>
</Symbols>
请注意,在位图部分中,我们列出了 bmpPic1,其值设置为 1。这告诉 Visual Studio 要使用位图条中的哪个图像。每个图像都是 16x16 像素,此值是(基于 1 的)要使用的图像索引。在此项目中,位图条中只有 1 个图像,因此我们使用 1 作为索引。
UpdateProjectPropertiesPackage
这个类是任何扩展的核心。Visual Studio Package Wizard 会创建这个文件的基本结构(总是命名为[ProjectName]Package.cs
),然后你需要自定义它来实现在你的扩展。我们为这个扩展所做的第一件事是让这个类继承IVsSolutionEvents3
接口,这样我们就可以监听OnAfterOpenSolution
和OnBeforeCloseSolution
事件,以知道何时启用或禁用按钮。为了告诉 Visual Studio 通知我们的类解决方案事件,我们将调用IVsSolution
对象的AdviseSolutionEvents
方法,稍后会详细介绍。该接口有很多方法,但我们不需要关心的只是确保我们返回VSConstants.S_OK
来告诉 VS 我们处理了事件。
public int OnBeforeOpeningChildren(IVsHierarchy pHierarchy)
{
return VSConstants.S_OK;
}
在需要启用/禁用按钮的两个方法(OnAfterOpenSolution
和OnBeforeCloseSolution
)中,我们只需设置按钮的 enabled 属性
/// <summary>
/// Enable the button when a solution has been opened.
/// </summary>
/// <param name="pUnkReserved"></param>
/// <param name="fNewSolution"></param>
/// <returns></returns>
public int OnAfterOpenSolution(object pUnkReserved, int fNewSolution)
{
if (_menuItem != null && _vsSolution != null)
{
_menuItem.Enabled = true;
}
return VSConstants.S_OK;
}
Initialize 方法
Initialize() 方法由 Visual Studio 调用,用于初始化我们的扩展。任何需要配置您的扩展并需要访问 Visual Studio API 的操作都必须在此方法中完成。您的扩展的构造函数可能会在 API 可用之前被 Visual Studio 调用,因此构造函数中的任何内容都不能依赖于任何 Visual Studio 对象或服务。
/// <summary>
/// Initialization of the package; this method is called right after the package is sited, so this is the place
/// where you can put all the initialization code that rely on services provided by VisualStudio.
/// </summary>
protected override void Initialize()
{
base.Initialize();
// Add our command handlers for menu (commands must exist in the .vsct file)
OleMenuCommandService mcs = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
if ( null != mcs )
{
// Create the command for the menu item.
CommandID menuCommandID = new CommandID(
GuidList.guidUpdateProjectPropertiesCmdSet,
(int)PkgCmdIDList.cmdUpdateProjectProperties
);
_menuItem = new OleMenuCommand(MenuItemCallback, menuCommandID);
_menuItem.Enabled = false;
mcs.AddCommand(_menuItem);
_dte = GetService(typeof(SDTE)) as DTE2;
// Hook the class up to receive solution event so we can enable/disable the button.
AdviseSolutionEvents();
}
}
我们对该方法的实现非常简单
- 我们获取对
OleMenuCommandService
的引用,以便我们可以将命令添加到其中。 - 我们创建一个
CommandID
(menuCommandID
),它使用菜单组的 Guid (GuidList.guidUpdateProjectPropertiesCmdSet
) 和按钮的 ID (PkgCmdIDList.cmdUpdateProjectProperties
) 引用我们在 .vsct 文件中定义的按钮。 - 我们创建一个
OleMenuCommand
来表示我们的按钮。在构造函数中,我们传入点击按钮时要调用的方法 (MenuItemCallback
) 和我们刚刚为按钮创建的CommandID
(menuCommandID
)。 - 我们将
OleMenuCommand
添加到OleMenuCommandService
。 - 我们调用将为我们的类注册解决方案事件通知的方法 (
AdviseSolutionEvents
)。
AdviseSolutionEvents 方法
此方法注册我们的类以接收解决方案事件通知。
/// <summary>
/// Subscribes to solution events.
/// </summary>
private void AdviseSolutionEvents()
{
UnadviseSolutionEvents();
_vsSolution = this.GetService(typeof(SVsSolution)) as IVsSolution;
if (_vsSolution != null)
{
_vsSolution.AdviseSolutionEvents(this, out _hSolutionEvents);
}
}
- 首先,我们调用
UnadviseSolutionEvents
方法,以确保我们只订阅一次。UnadviseSolutionEvents
只是此方法的反向操作,因此无需详细介绍。 - 我们获取对解决方案服务 (
IVsSolution
) 的引用。 - 我们调用服务的
AdviseSolutionEvents
方法来连接我们的类,以便它将接收解决方案事件的通知。
MenuItemCallback 方法
此方法在点击我们的按钮时由 Visual Studio 调用,并且不言自明。我们所做的只是调用一个辅助方法来获取所有项目的列表,然后将其传递给我们用来完成实际工作的表单。
/// <summary>
/// This function is the callback used to execute a command when the a menu item is clicked.
/// See the Initialize method to see how the menu item is associated to this function using
/// the OleMenuCommandService service and the MenuCommand class.
/// </summary>
private void MenuItemCallback(object sender, EventArgs e)
{
if (_vsSolution != null)
{
// Get the list of projects in the solution and open the form:
using (fUpdateSettings frm = new fUpdateSettings(new List<Project>(GetProjects(_vsSolution))))
{
frm.ShowDialog();
}
}
}
我不会在这里深入探讨辅助方法,只需查看源代码即可。它们基本上只是遍历解决方案的项目层次结构并提取项目列表。
fUpdateSettings
此表单允许用户输入并设置属性值。所有方法都相当直观,所以我只介绍我们用于更新项目属性的方法。表单的构造函数接受我们在打开表单之前在 UpdateProjectPropertiesPackage
类中检索到的当前解决方案中的项目列表。
SetProperty 方法
在此方法中,我们只是遍历项目列表,然后遍历每个项目中的每个属性,并设置我们想要设置的属性。
/// <summary>
/// Sets the value of a property in all projects of open solution.
/// </summary>
/// <param name="propertyName">Name of the property to set.</param>
/// <param name="value">New value of the property.</param>
private void SetProperty(string propertyName, string value)
{
try
{
if (_projects.Count > 0)
{
//Go through each project in the Solution:
foreach (Project prj in _projects)
{
//Check each property:
foreach (Property prop in prj.Properties)
{
//Set the property:
if (prop.Name == propertyName)
{
prop.Value = value;
}
}
}
}
}
catch (Exception ex)
{
MessageBox.Show(string.Format("An error occurred trying to set the {0}.\r\n{1}", propertyName, ex.Message), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
关注点
所有这一切唯一真正需要注意的是,确保您用于订阅解决方案事件的对象在您需要它之前不会被 Visual Studio 垃圾回收。Visual Studio 有一个讨厌的倾向,除非它直接持有引用,否则它会处理掉对象。其他人会告诉您可以创建一个对象来实现 IVsSolutionEvents3 接口,并简单地将其保存在类级别字段中,它就会收到解决方案事件的通知,但在实践中我发现即使它们保存在类级别字段中,Visual Studio 也会忘记事件订阅者。这就是我直接在包类 (UpdateProjectPropertiesPackage
) 中实现 IVsSolutionEvents3
接口的原因。
历史
- 2015-08-02:第一个版本。