:::: MENU ::::

Tuesday, August 7, 2012

Quite a while ago I presented a scrappy little macro I created to update version numbers in multiple Visual Studio projects. At the time I commented that Visual Studio 11 wouldn't be supporting macros so, now that VS2012 has RTM'd, here's a "port" to a C# version, using the Visual Studio Extensibility mechanisms.

The starting point is to use the project wizard to create a Visual Studio Add-in (to be found in the Extensibility section of the template list). I chose C# as my programming language in the first step and Visual Studio 2012 as the application host in the next (the techniques work equally well for Visual Studio 2010). The next page asks for a name and description; on page 4, I specified yes to a tools menu item; and on the fifth page I felt a bit lazy and omitted the about box. The wizard creates a bunch of boilerplate code, the most interesting bit of which is the Exec method near the end of Connect.cs: as the name might suggest, this is the routine that gets run when you click the button, select the menu item or otherwise invoke the new command. Here's mine, with my additions in bold:

public void Exec(string commandName, vsCommandExecOption executeOption, ref object varIn, ref object varOut, ref bool handled)
{
handled = false;
if(executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault)
{
if(commandName == "VersionUpdate.Connect.VersionUpdate")
{
using (var w = new MainForm(_applicationObject))
{
w.ShowDialog();
}
handled = true;
return;
}
}
}

I'm showing a dialog box, in which the user can specify version information - I'd better describe that MainForm... It's a Windows Forms window which contains the same set of controls I used in the macro from last time: a checked list box in which to display found version information, a text box into which to type a new version number, and apply and cancel buttons. (You'll need to add System.Windows.Forms to project references to be able to use these objects.)

The form constructor iterates over all projects in the solution, adding version data to the list - pretty much the same as the macro, but with a few minor changes in syntax and where resources are found:

private void PopulateList(EnvDTE80.DTE2 dte)
{
var sol = dte.Solution;
foreach (Project proj in sol.Projects)
{
dynamic csp = proj;
foreach (var propertyName in new string[] { "AssemblyVersion", "AssemblyFileVersion" })
{
try
{
var version = proj.Properties.Item(propertyName).Value as string;
if (!string.IsNullOrWhiteSpace(version))
{
var versionRef = new VersionReference { Project = proj, Id = propertyName, Version = version };
this.listProjects.Items.Add(versionRef, true);
}
}
catch
{
}
}
} // end foreach proj
}

The argument to this function is the _applicationObject passed into MainForm's constructor and offers access to the Visual Studio object model. Rather than work out what type of project each is to determine if the version information is available, I've been incredibly lazy: I cast the project to "dynamic" and then wrap accesses through that object in a try...catch block (which would be tripped when the object doesn't support property access - other errors will trip it too, of course... did I mention I was lazy?). The VersionReference class mentioned in there is just a holder for the three items indicated, with a ToString override to show the project name, version property name and value, that ends up automatically being used when rendering the list (less effort than providing a custom data template):

internal class VersionReference
{
public override string ToString()
{
return string.Format("{0}, {1}={2}", Project.Name, Id, Version);
}

internal EnvDTE.Project Project { get; set; }
internal string Id { get; set; }
internal string Version { get; set; }
}

To complete the operation, the OK button invokes:

private void btnOK_Click(object sender, EventArgs e)
{
foreach (VersionReference v in this.listProjects.CheckedItems)
v.Project.Properties.Item(v.Id).Value = txtVersion.Text;

this.Close();
}

Putting all that together gives you a new command which can be invoked via the command window, or via a menu item - once it's been registered. I've not investigated building an installer for the add-in yet, but the wizard will register it on the development machine, which is good enough for me for the time being. The process is explained on MSDN.

I've also not changed the icon from the default. Connect.OnConnection contains calls to AddNamedCommand2 for each command you add: a pair of the parameters identify the icon to be used - the fifth indicates if the icon is internal or custom, and the one after identifies the icon. The default smiley face is number 59, it seems. There are acouple of pages on MSDN explaining how to change the icon - a small job for another day, I'll stick with a smiley face until I get bored with it.

The code here is very very close to that in the macro version I wrote before, which isn't too surprising since most of it involves traversing object models that are the same in both cases. There's a minor syntax translation from VB to C# - if I'd thought about it sooner, I could have written this add-in in VB.NET and copied and pasted chunks of code directly.

Deploying the add-in is a case of copying the binary and the .addin file into an appropriate location on the destination machine as described in MSDN documentation. Maybe I'll get round to creating an installation package and publishing it on the Visual Studio gallery, but don't hold your breath waiting for that to happen!

More