My first few blog posts were essentially about taking a macro (almost) solution and reimplementing it in C#: this time I'm taking an approach to a problem usually solved using some compiled language and implementing it as a cheap and cheerful macro. It's a bit unfortunate that I'm posting this at the same time as macros seem to be vanishing from the next version of Visual Studio but, hey, it's been written and I'm not going to waste it!

A common problem is when you have some application consisting of a number of binaries, and you'd really like to have them labelled consistently. The typical C/C++ approach is to have a single common include file, and update only that. In C#/VB you can do something similar by adding a link to a common AssemblyInfo file, but that seems to sometimes result in the Visual Studio IDE getting a bit confused. Another common approach is to have some external program, run just before a build, which scurries over all projects and updates the version text - that's pretty much what I've done here, except my program is a macro running within Visual Studio. The "running within VS" bit is important here - this means that the execution of the program automatically integrates with whatever version control system VS is using, such that files are checked out as required.

Let's get stuck in: open up the macros IDE in VS (under the Tools -> Macro menu) and add a module called VersionUpdate to the MyMacros node. Here's some code to print out all attributes of all C# projects in the current solution's projects:

Sub PrintAttributes()
Dim sol As Solution = DTE.Solution

For Each proj As Project In sol.Projects
If proj.Kind = VSLangProj.PrjKind.prjKindCSharpProject Then
Debug.Print("C# project {0}", proj.FullName)
Dim csp As VSLangProj.VSProject = proj.Object
For i As Integer = 1 To proj.Properties.Count - 0
Try
Debug.Print("{0}: {1} = '{2}'", i, proj.Properties.Item(i).Name, proj.Properties.Item(i).Value)
Catch
Debug.Print("{0}: {1} failed", i, proj.Properties.Item(i).Name)
End Try
Next
End If
Next
End Sub

You should be able to do similar for VB projects, but this doesn't work for C++ unfortunately - the project interface doesn't seem to be the same as for C#/VB. You'll see AssemblyVersion and AssemblyFileVersion attributes, which are obviously what we're looking for.

I can never remember precisely what each represents, and to save myself the effort of working it out, I lazily just make them the same for builds I'm releasing outside the group, to be absolutely certain what I'm working with - there are many other, and better, strategies for setting the versions and, once you've got access to them via the VS extensibility interfaces.

My macro iterates over projects as above, adding each project's version information to a list; then presents that list in a dialog box which also contains a field in which I can enter a new version string; hitting the "update" button then sets all those version attributes, which will update the projects' AssemblyInfo files (or wherever the version strings actually are). Simples!

First, here's my type for storing the version attribute information that I find in the first step:

Class VersionReference
Public ReadOnly Property Text() As String
Get
Return String.Format("{0}, {1}={2}", Project.Name, Id, Version)
End Get
End Property
Public Project As Project
Public Id As String
Public Version As String
End Class

The name and current version string are for human consumption; the important bit is the id, through which I can update the version string.

Doing UI in a VS macro is fairly easy: import System.Windows.Forms and then build up the UI the same way you would in a VB project (alas, there's no GUI designer, but you can create a VB project, then copy most of what you need from there). Here's the start of my UpdateVersions macro, which builds the dialog box:

Sub UpdateVersions()
Const width As Integer = 600
Const height As Integer = 500
Const buttonWidth As Integer = 100
Const buttonHeight As Integer = 40
Const padding As Integer = 10

Dim dlg As New Form()
Dim list As New CheckedListBox()
Dim btnOK As New Button
Dim btnCancel As New Button
Dim lblVersion As New Label
Dim txtVersion As New TextBox

dlg.Text = "Version information"
dlg.ClientSize = New System.Drawing.Size(width, height)

btnOK.Text = "OK"
btnOK.DialogResult = DialogResult.OK
btnOK.Left = padding
btnOK.Top = height - buttonHeight - padding
btnOK.Width = buttonWidth
btnOK.Height = buttonHeight
btnOK.Anchor = AnchorStyles.Bottom Or AnchorStyles.Left

btnCancel.Text = "Cancel"
btnCancel.DialogResult = DialogResult.Cancel
btnCancel.Left = width - buttonWidth - padding
btnCancel.Top = btnOK.Top
btnCancel.Width = buttonWidth
btnCancel.Height = buttonHeight
btnCancel.Anchor = AnchorStyles.Bottom Or AnchorStyles.Right

lblVersion.Text = "New version"
lblVersion.Left = padding
lblVersion.Top = btnOK.Top - buttonHeight - padding
lblVersion.Width = buttonWidth
lblVersion.Height = buttonHeight
lblVersion.TextAlign = Drawing.ContentAlignment.MiddleLeft

txtVersion.Text = "1.0.0.0"
txtVersion.Left = width - buttonWidth - padding
txtVersion.Top = lblVersion.Top
txtVersion.Width = buttonWidth
txtVersion.Height = buttonHeight

list.DisplayMember = "Text"
list.CheckOnClick = True
list.Left = padding
list.Top = padding
list.Width = width - padding * 2
list.Height = lblVersion.Top - padding
list.Anchor = AnchorStyles.Bottom Or AnchorStyles.Left Or AnchorStyles.Right Or AnchorStyles.Top

dlg.Controls.Add(list)
dlg.Controls.Add(lblVersion)
dlg.Controls.Add(txtVersion)
dlg.Controls.Add(btnOK)
dlg.Controls.Add(btnCancel)

Next, a loop very similar to the first code fragment above, to populate the list box with appropriate version information:

    Dim sol As Solution = DTE.Solution
For Each proj As Project In sol.Projects
If proj.Kind = VSLangProj.PrjKind.prjKindCSharpProject Then
Dim csp As VSLangProj.VSProject = proj.Object
For Each p As String In New String() {"AssemblyVersion", "AssemblyFileVersion"}
Try
Dim v As New VersionReference()
v.Project = proj
v.Id = p
v.Version = proj.Properties.Item(p).Value
list.Items.Add(v, True)
Catch
End Try
Next
End If
Next

The VersionReference class's ToString method is a trivially easy way to get the list box to display the current version information.

The final part of UpdateVersions is to show the dialog box and then, if OK was pressed, update all those version attributes with the string specified by the user:

    If dlg.ShowDialog() = DialogResult.OK Then
For Each v As VersionReference In list.CheckedItems
Debug.Print("Updating {0}, {1}", v.Project.FullName, v.Id)
v.Project.Properties.Item(v.Id).Value = txtVersion.Text
Next
End If
End Sub

Not much to it! The macro can be invoked via the macros IDE, but it's more convenient to have it on a toolbar, which you can do via VS UI customization - all your macros will be listed under Macros (what a surprise!) in the add command option.

That was pretty easy: it ought to be fairly easy to migrate this to a "proper" VS extension project when the time comes - perhaps the topic of a future blog post, when I've had time to explore VS 11 properly.