Introduction
This is a simple little plugin (.vsix) for Visual Studio 2012 that will batch-update the properties of all projects in a solution and is a useful introduction to writing extensions for Visual Studio.
Contents
- Background
- Using the Demo
- Using the Code
- UpdateProjectProperties.vsct
- UpdateProjectPropertiesPackage
- The Initialize Method
- The AdviseSolutionEvents Method
- The MenuItemCallback Method
- fUpdateSettings
- The SetProperty Method
- Points of Interest
- History
Ever get frustrated when you make changes to a solution and have to go through each project one-by-one to change the version number, or you created an app for a client and have to change the company name & copyright info for each project one-by-one? I certainly do, it's just a tedious waste of time that needlessly eats up time before you can click 'Build'.
To install the demo unzip the demo and open a command-prompt in the C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\IDE folder. Now type: vsixinstaller "full path to vsix file". This will install the extension into Visual Studio (If Visual Studio is open while you install you may have to restart it to use the extension).
The extension creates a button in the Solution Explorer's toolbar:
When you click the button you'll get a simple screen where you can enter property values and set them: just enter a value and click 'Set' next to the value to apply it to all the projects in the solution.
Before you can create an extension for Visual Studio you have to install the Visual Studio SDK. This will add a 'Visual Studio Package' template under Extensibility when you create a new project. Visual Studio creates a whole bunch of files when you run the Visual Studio Package Wizard. There are only 2 that we're really insterested in: the UpdateProjectProperties.vsct file and the UpdateProjectPropertiesPackage.cs file. Apart from customizing these two files all I needed to do was add a form (fUpdateSettings.cs) to provide a UI and update the selected properties.
Command-table files (.vsct) describes the set of commands that are contained in a VSPackage. In our case we need to add a button with an icon to the Solution Explorer's toolbar.
First we create a menu group for the button and tell Visual Studio that the group should be added to the Solution Explorer's toolbar:
<group guid="guidUpdateProjectPropertiesCmdSet" id="SolutionToolbarGroup" priority="0xF000">
<parent guid="guidSHLMainMenu" id="IDM_VS_TOOL_PROJWIN">
</parent>
The Parent element is what tells VS where to add the group, IDM_VS_TOOL_PROJWIN
is the constant for the Solution Explorer's toolbar.
Next we define an image for the button:
<!---->
<Bitmaps>
<!---->
<Bitmap guid="guidImages" href="Resources\drawing4.png" usedList="bmpPic1"/>
</Bitmaps>
The href
points to an image file included in the project as an embedded resource and usedList
points to a a symbol defined in the <Symbols>
section of the file.
Next we define the actual button:
<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>
We define the button and tell Visual Studio that it's parent is the menu group we defined earlier, what image to use for the button and what the text of the button should be.
Finally we put it all together in the <Symbols>
section of the file which defines the Guids and IDs that are used by the other elements in the vsct file:
<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>
Note that in the bitmap section where we list bmpPic1 the value is set to 1. This tells Visual Studio which image in the bitmap strip to use. Each image is 16x16 pixels and this value is the index (1-based) of the image to use. In this project the bitmap strip only has 1 image so we use 1 as the index.
This class is the core of any extension. The Visual Studio Package Wizard creates the basics of this file (always named [ProjectName]Package.cs
) which you then need to customize to implement your extension. The first thing we had to do for this extension is make the class inherit the IVsSolutionEvents3
interface so that we can listen for the OnAfterOpenSolution
and OnBeforeCloseSolution
events to know when to enable or disable the button. To tell Visual Studio to notify our class of solution events we will call the AdviseSolutionEvents
method of the IVsSolution
object, more about this later. The interface has quite a few methods but all we need to do in the ones we're not interested in is ensure that we return VSConstants.S_OK
to tell VS that we handled the event:
public int OnBeforeOpeningChildren(IVsHierarchy pHierarchy)
{
return VSConstants.S_OK;
}
In the 2 mehods we need to enable/disable the button (OnAfterOpenSolution
and OnBeforeCloseSolution
) we just set the button's enabled property:
public int OnAfterOpenSolution(object pUnkReserved, int fNewSolution)
{
if (_menuItem != null && _vsSolution != null)
{
_menuItem.Enabled = true;
}
return VSConstants.S_OK;
}
The Initialize() method is called by Visual Studio to initialize our extension. Anything you need to do to configure your extension that requires access to the Visual Studio api must be done in this method. The constructor of your extension may be called by Visual Studio before the api becomes available so nothing in the constructor can depend on any Visual Studio objects or services.
protected override void Initialize()
{
base.Initialize();
OleMenuCommandService mcs = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
if ( null != mcs )
{
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;
AdviseSolutionEvents();
}
}
Our implementation of the method is quite simple:
- We get a reference to the
OleMenuCommandService
so we can add our command to it.
- We create a
CommandID
(menuCommandID
) which references the button we defined in the .vsct file using the menu group's Guid (GuidList.guidUpdateProjectPropertiesCmdSet
) and the id of the button (PkgCmdIDList.cmdUpdateProjectProperties
).
- We create an
OleMenuCommand
to represent our button. In the constructor we pass the method to call when the button is clicked (MenuItemCallback
) and the CommandID (menuCommandID
) we just created for the button.
- We add our
OleMenuCommand
the the OleMenuCommandService
.
- We call the method which will enroll our class for notification of Solution events (
AdviseSolutionEvents
).
This method enrolls our class for notification of solution events.
private void AdviseSolutionEvents()
{
UnadviseSolutionEvents();
_vsSolution = this.GetService(typeof(SVsSolution)) as IVsSolution;
if (_vsSolution != null)
{
_vsSolution.AdviseSolutionEvents(this, out _hSolutionEvents);
}
}
- First we call the
UnadviseSolutionEvents
method to make sure we're only subscribed once. UnadviseSolutionEvents
is just the reverse of this method so there's no need to go into it in detail.
- We get a reference to the solution service (
IVsSolution
).
- We call the service's
AdviseSolutionEvents
method to hook up our class so that it will receive notification of solution events.
This method is called by Visual Studio when our button is clicked and is pretty self-explanatory. All we're doing is calling a helper method to get a list of all the projects and passing this to the form we'll use do do the real work:
private void MenuItemCallback(object sender, EventArgs e)
{
if (_vsSolution != null)
{
using (fUpdateSettings frm = new fUpdateSettings(new List<Project>(GetProjects(_vsSolution))))
{
frm.ShowDialog();
}
}
}
I won't go into the helper methods here, just have a look at the source code. All they basically do is go through the solution's project hierarchy and extract the list of projects.
This form is where we allow the user to enter property values and set them. All the methods are pretty straightforward so I'll only go into the method we're using to update the project properties. The form's constructor takes a list of projects in the current solution that we retrieved in our UpdateProjectPropertiesPackage
class before opening the form.
In this method we simply iterate through the list of projects and then through each property in each project and set the one we want to set:
private void SetProperty(string propertyName, string value)
{
try
{
if (_projects.Count > 0)
{
foreach (Project prj in _projects)
{
foreach (Property prop in prj.Properties)
{
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);
}
}
The only real catch with all of this is to make sure that the object you use to subscribe to solution events is not garbage-collected by Visual Studio before you need it. Visual Studio has a nasty tendency to dispose of things unless it is directly holding a reference to it. Others will tell you you can create an object to implement the IVsSolutionEvents3 interface and simply hold it in a class-level field and it will get notified of solution events, in practise I've found that Visual Studio will forget about event-subscribers even if they're held in a class-level field. This is why I implemented the IVsSolutionEvents3
interface directly in the package class (UpdateProjectPropertiesPackage
).
- 2015-08-02: First Version.