Visual Studio macro to update version numbers

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.