How To: Use Macros to Configure Publish Settings

There are all sorts of publishing properties available to the Visual Studio developer. Visual Studio tries to set the properties to reasonable default values. But with all things that are defaulted, the values are right for some people, but wrong for others. In my day-to-day work, where I run through publishing quite a bit, there are quite a few values I want to reset. Its possible to click through the UI to change the values, but who has a couple of seconds to spare to open dialogs and click on stuff? Not I; every second lost is a second I could spend writing a blog entry. Fortunately, its easy to create a macro that will set these values for you. In this post, I'll write about you can use a macro to set these default values through the DTE.

Displaying publish properties and values

Let's start by viewing all available publishing properties. Before going too far, let's show some helper functions to accomplish our tasks. Here is some code I copied from my macro explorer:

 Function GetPublishProperties() As EnvDTE.Properties
    Dim proj As Project = DTE.Solution.Projects.Item(1)
    If proj Is Nothing Then
        ShowError("Unable to retrieve a project", "GetPublishProperties")
        Return Nothing
    End If

    Dim publishProperty As EnvDTE.Property = proj.Properties.Item("Publish")
    If publishProperty IsNot Nothing Then
        Dim publishProperties As EnvDTE.Properties = TryCast(publishProperty.Value, EnvDTE.Properties)
        Return publishProperties
    End If

    ShowError("Unable to get publish properties of project " & proj.Name, "GetPublishProperties")
    Return Nothing
End Function

Sub ShowError(ByVal message As String, ByVal title As String)
    MsgBox(message, MsgBoxStyle.Exclamation Or MsgBoxStyle.OkOnly, title)
End Sub

Sub ShowException(ByVal ex As Exception, ByVal methodName As String)
    Dim message As String = ex.Message
    If ex.InnerException IsNot Nothing Then
        message = ex.InnerException.Message
    End If
    ShowError(message, methodName)
End Sub

GetPublishProperties is a function to retrieve the object which contains the publish properties. It grabs the first project in the solution, and selects the "Publish" property from there. Next, the resultant property value is converted to another properties container. Some error handling is also included (although probably not enough).

With that as a baseline, this macro will show all of the publish properties, types, and values:

 Sub ShowPublishProperties()
    Dim publishProperties As EnvDTE.Properties = GetPublishProperties()
    If publishProperties IsNot Nothing Then
        Dim sb As New System.Text.StringBuilder()
        For Each prop As EnvDTE.Property In publishProperties
            sb.Append(String.Format("{0} [{1}]: {2}", prop.Name, prop.Value.GetType().ToString(), prop.Value.ToString()))
            sb.Append(vbCrLf)
        Next
        MsgBox(sb.ToString())
    End If
End Sub

This macro simply iterates through all of the publish properties, gathers the property names, types, and values and show them all in a message box. Here is a subset of the output:

 PublisherName [System.String]: 
OpenBrowserOnPublish [System.Boolean]: True
BootstrapperComponentsLocation [System.Int32]: 0
PublishFiles [System.__ComObject]: System.__ComObject

Most of the values are strings, ints, or booleans. If you were to look in the project file, you would see that these values correspond to properties within the file. The exception are PublishFiles and BootstrapperPackages, which correspond to the Items in the project file, as well as the the lists within the Application Files dialog and Prerequisites dialog. These will be explored further in a bit. But first, let's try setting some easy values.

Setting Property values

Setting a value is as easy as reading a value. For example, to set the PublisherName property, one could write a macro like this:

 Sub SetPublisherName()
    Dim publishProperties As EnvDTE.Properties = GetPublishProperties()
    If publishProperties IsNot Nothing Then
        Dim publisherNameProperty As EnvDTE.Property = publishProperties.Item("PublisherName")
        publisherNameProperty.Value = "Test Value"
    End If
End Sub

Some properties perform validation when the values are set. For example, the PublishUrl:

 Sub SetPublishUrl()
    Dim publishProperties As EnvDTE.Properties = GetPublishProperties()
    If publishProperties IsNot Nothing Then
        Try
            publishProperties.Item("PublishUrl").Value = ""
        Catch ex As Exception
            ShowException(ex, "SetPublishUrl")
        End Try
    End If
End Sub

In the above example, an attempt is made to blank out the PublishUrl property. However, the PublishUrl can not be set to an empty string, as the thrown exception indicates:

 An empty string is not allowed for property 'Publish Location'.

The thrown message uses "Publish Location" because that is what the label for the text box which corresponds to this property uses. In fact, the property page is pretty much setting this property in the exact same manner as the macro.

Modifying Items

There are 2 different types of Items exposed by the Publish

Properties: PublishFiles and BootstrapperPackages. Here is a macro that does something with PublishFiles:

 Sub ListFiles()
    Dim publishProperties As EnvDTE.Properties = GetPublishProperties()
    If publishProperties IsNot Nothing Then

        Dim filesObject As Object = publishProperties.Item("PublishFiles")
        If filesObject Is Nothing Then
             ShowError("Could not get PublishFiles object", "ListFiles")
            Return
        End If

        Try
            Dim numFiles As Integer
            numFiles = filesObject.Value.Item("Count").Value
            Dim sb As New System.Text.StringBuilder()
            For i = 0 To numFiles - 1
                'display file name and publish status.
                sb.AppendLine("File Name=" & filesObject.Object.Item(i).Name & ", Status=" & filesObject.Object.Item(i).PublishStatus)
            Next
            MsgBox(sb.ToString(), MsgBoxStyle.DefaultButton1, "Application Files")
        Catch ex As Exception
            ShowException(ex, "ListFiles")
        End Try
    End If
End Sub

This macro relies on latebinding in doing its work: filesObject is declared as Object, yet it uses properties from that object. The filesObject.Object contains an accessor to a collection; there are actually 2 ways to get a value out of the collection: one could use either a string to get a file by name, or an index, like what was done above. Here is some sample output from running this macro:

 File Name=WindowsApplication2.exe, Status=0 
File Name=WindowsApplication2.pdb, Status=0 
File Name=WindowsApplication2.xml, Status=0 

The "Status" of the file corresponds to the Publish Status column in the Application Files dialog. 0 corresponds to the "(Auto)" value.

Accessing the bootstrapper packages is similar:

 Sub IncludeAllPrerequisites()
    Dim publishProperties As EnvDTE.Properties = GetPublishProperties()
    Dim bootstrapperPackages As Object = publishProperties.Item("BootstrapperPackages")
    If bootstrapperPackages IsNot Nothing Then
        Dim numPackages As Integer = bootstrapperPackages.Value.Item("Count").Value
        For i As Integer = 0 To numPackages - 1
            bootstrapperPackages.Object.Item(i).Install = True
        Next
    End If
End Sub

Sub ExcludeWindowsInstaller31()
    Dim publishProperties As EnvDTE.Properties = GetPublishProperties()
    Dim bootstrapperPackages As Object = publishProperties.Item("BootstrapperPackages")
    If bootstrapperPackages IsNot Nothing Then
        Dim windowsInstaller31Package As Object = bootstrapperPackages.Object.Item("Microsoft.Windows.Installer.3.1")
        If windowsInstaller31Package IsNot Nothing Then
            windowsInstaller31Package.Install = False
        End If
    End If
End Sub

The first macro includes all available prerequisites when publishing. In the first example, the packages are accessed by index. The second example gets a bootstrapper package by using a specific product code.

Publishing via a macro

Finally, just like it is possible to build from a macro, it is possible to publish as well. Here is an example that does something similar to what the Publish Now button does on the Publish property page:

 Sub Publish()
    Dim proj As Project = DTE.Solution.Projects.Item(1)
    Dim sb2 As EnvDTE80.SolutionBuild2 = CType(DTE.Solution.SolutionBuild, EnvDTE80.SolutionBuild2)
    Dim config2 = CType(sb2.ActiveConfiguration, EnvDTE80.SolutionConfiguration2)
    Dim configName = String.Format("{0}|{1}", config2.Name, config2.PlatformName)
    sb2.BuildProject(configName, proj.UniqueName, True)
    sb2.PublishProject(configName, proj.UniqueName, True)
End Sub

Ideally, the macro should verify that the build succeeded before publishing, but that has been left as an exercise for the reader.