Generating multiple versions of ClickOnce applications with Team Build

It is very common to have a need to run multiple versions of a ClickOnce application, each pointing to different environment (Dev, QA, Prod, etc).  There are plenty of resources on the web that show you how to do this manually (for example, this one: http://robindotnet.wordpress.com/2009/04/22/clickonce-installing-multiple-versions-concurrently/).  Basically, you have to change the assembly name and the product name.  But what if you need to do it as part of your automated build in TFS?

Let’s start with what happens when you change these properties in the IDE…all these settings are part of the project file (YourProject.csproj).  The project file is just a big xml file that contains all the information that your project needs in order to build your application.  My first inclination was that I can tell Team Build to run the msbuild task with the the /p:AssemblyName=NewName switch, but this will only work if your solution only has one project in it, otherwise that switch will try to overwrite all your assemblies with that name, and of course you will run into invalid references and a broken build. 

So the next option is to add a custom build activity and make it part of your build process.  This may sound difficult, but it’s really straight forward.  Here is a nice series that takes you to the process of customizing Team Build 2010: http://www.ewaldhofman.nl/post/2010/04/20/Customize-Team-Build-2010-e28093-Part-1-Introduction.aspx

So assuming that you read that or that you know how the process works, this will make sense.  You need to start by creating a class library and you can start implementing your activity.  Here is how I did it:

  • I have two imput arguments: AssemblyFilePath, and NewAssemblyName.   Those two parameters should be set in the build template so that this activity know what to act on.  You could hard-code those values, but ideally, you are building your activities so that they can be reused by other build processes.
  • Use LINQ to XML to parse through the project file to find the AssemblyName element
  • Updated it and save the file again.
  • You then open up the build template and drop this activity right after the workspace has been created and the build does a GetLatest.
  • I don’t check in my changes, as I only want them to be applied to my build, so the code changes will be wiped out during the next build.
using System;
using System.IO;
using System.Linq;
using System.Activities;
using System.Text.RegularExpressions;
using Microsoft.TeamFoundation.Build.Client;
using Microsoft.TeamFoundation.Build.Workflow.Activities;
using Microsoft.TeamFoundation.VersionControl.Client;
using System.Xml.Linq;

namespace Esteban.BuildTasks.Activities
{
    /// 
    /// Updates the assembly name that will be generated when a project is built. 
    /// 
    /// This is useful for click-once applications targetting different environments. 
    /// 
    /// 
    [BuildActivity(HostEnvironmentOption.All)]
    public sealed class ChangeAssemblyName : CodeActivity
    {
        /// 
        /// The path to the project file.
        /// 
        [RequiredArgument]
        public InArgument<string> ProjectFilePath { get; set; }

        /// 
        /// Assembly name that should be used.
        /// 
        public InArgument<string> NewAssemblyName { get; set; }

        /// 
        /// The workspace that is used by the build
        /// 
        [RequiredArgument]
        public InArgument Workspace { get; set; }


        protected override void Execute(CodeActivityContext context)
        {

            var projectFilePath = context.GetValue(this.ProjectFilePath);
            var workspace = context.GetValue(this.Workspace);
            var newAssemblyName = context.GetValue(this.NewAssemblyName);

            //Get the local project file.
            var localProjectFilePath = workspace.GetLocalItemForServerItem(projectFilePath);

            if (!string.IsNullOrWhiteSpace(localProjectFilePath))
            {
                //Clear the read-only attribute on the file
                if ((File.GetAttributes(localProjectFilePath) & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
                {
                    File.SetAttributes(localProjectFilePath, File.GetAttributes(localProjectFilePath) & ~FileAttributes.ReadOnly);
                }

                XDocument doc = XDocument.Load(localProjectFilePath);

                var assemblyElement = (from item in doc.Elements().Descendants()
                                       where item.Name.LocalName == "AssemblyName"
                                       select item).Single();
                
                assemblyElement.Value = newAssemblyName;

                doc.Save(localProjectFilePath);
            }
            else
            {
                throw new BuildProcessException("Project file was not found.");
            }
        }
    }
}
Scroll to Top