How To: Publish Output Groups from Other Projects in a ClickOnce Project

In addition to output from a "compile", projects are capable of creating other outputs. Setup projects can consume the output from other projects, package them up, and install them as part of the generated Windows Installer package. These groups of different outputs are referred to as, well, "output groups", and are exposed through the DTE as the following macro demonstrates:

 Sub ListAll() 
    For Each proj As Project In DTE.Solution.Projects 
        Dim groupInfo As New StringBuilder() 
        If proj.ConfigurationManager IsNot Nothing Then 
            For Each group As OutputGroup In proj.ConfigurationManager.ActiveConfiguration.OutputGroups 
                groupInfo.AppendLine(String.Format("{0}: {1} ({2})", group.CanonicalName, group.DisplayName, group.FileCount)) 
            Next 
            MsgBox(groupInfo.ToString(), MsgBoxStyle.OkOnly, proj.Name) 
        End If 
    Next 
End Sub

The results for a very basic Winform Application is shown below:

Project Output Groups

It is a design limitation decision that ClickOnce projects are capable of publishing all of these output groups from its own project, but if you want the content, source, symbols, or documentation files from a different project in the solution, you are just plain out of luck. However, if you are willing to modify your project file just a little bit, your luck has just changed. Let's build upon an entry from last month and have a ClickOnce project publish output from other projects.

Its not well advertised, but there are Targets within Microsoft.Common.targets which expose the output groups of a project. For example, here is the Target which exposes the Content Files output group:

 <Target 
            Name="ContentFilesProjectOutputGroup" 
            Outputs="@(ContentFilesProjectOutputGroupOutput)" 
            DependsOnTargets="$(ContentFilesProjectOutputGroupDependsOn)"> 
 
    <!-- Convert items into final items; this way we can get the full path for each item. --> 
    <ItemGroup> 
        <ContentFilesProjectOutputGroupOutput Include="@(ContentWithTargetPath->'%(FullPath)')"/> 
    </ItemGroup> 
</Target>

It would be tempting to simply add this Target as a dependency to the BeforePublish target used in the project file. However, doing so would get the contents of this project, not some other project. What we really need is to run MSBuild on a different project, call this target, and consume that target's outputs. And MSBuild has a way to do exactly that: The MSBuild task.

By using the MSBuild task, it is possible to modify the BeforePublish target to consume the outputs of another project. Something like this should do the trick:

 <ItemGroup> 
    <PublishPOG Include="..\\ClassLibrary1\\ClassLibrary1.vbproj"> 
        <Visible>false</Visible> 
        <OutputGroup>DebugSymbolsProjectOutputGroup</OutputGroup> 
    </PublishPOG> 
    <PublishPOG Include="..\\ClassLibrary2\\ClassLibrary2.vbproj"> 
        <Visible>false</Visible> 
        <OutputGroup>ContentFilesProjectOutputGroup</OutputGroup> 
    </PublishPOG> 
</ItemGroup> 
<Target Name="BeforePublish"> 
    <Touch Files="@(IntermediateAssembly)" /> 
    <MSBuild Projects="%(PublishPOG.Identity)"  Targets="%(OutputGroup)"> 
        <Output TaskParameter="TargetOutputs"  ItemName="_DeploymentManifestFiles" /> 
    </MSBuild> 
</Target>

This change consumes the outputs of two different projects: the debug symbols from ClassLibrary1 and the content files from ClassLibrary2 (why anyone would want to publish the pdbs of another project is left as an exercise to the reader). This information is stored as an item with identity of the project to build and the output group to consume. BeforePublish then calls the MSBuild task to append the outputs onto the _DeploymentManifestFiles item, which is consumed by the GenerateApplicationManifest task (amongst others). So, which outputs are available for consumption? Here is a handy chart for reference:

Output Type MSBuild Target Name
Content Files ContentFilesProjectOutputGroup
Source Files SourceFilesProjectOutputGroup
Debug Symbols DebugSymbolsProjectOutputGroup
Documentation Files DocumentationProjectOutputGroup
Localized resources SatelliteDllsProjectOutputGroup
Serialization assemblies SGenFilesOutputGroup
Primary output BuiltProjectOutputGroup

Note that the satellite dlls, serialization assemblies, and primary output of another project are generally included simply by adding a reference to that other project. But if you do go down this route for these output types, you should append the output to the _DeploymentManifestDependencies item instead of _DeploymentManifestFiles.

All of this talk of "output groups" and "ClickOnce" naturally (I think) leads to another question. I don't want to ask it because I don't want to answer it (right now). But if someone wants to ask, feel free to leave a comment. Its a question I want to answer with a later post, but we'll see if it happens.