ClickOnce – Creating Publish Page from msbuild

ClickOnce via msbuild

The Problem

When you publish from Visual Studio, the publish directory gets three files:-

  • Publish.htm
  • setup.exe
  • MyApp.application

but when you publish using msbuild you do not get the Publish.htm.

This post covers one approach to ensure you also get a nice publish.htm file. Actually it’s a two part blog, in the second part I’ll cover how to simplify your deployment process for multiple environments.

As a pre-requisite you’ll need to install msbuild community tasks.

All credit for this post belongs to my good friend Craig Hunter, this is pretty much a slightly modified version of his solution.

Step One – Create Customization File

This step in kind of optional. All subsequent steps may be done within your .csproj, it is just my preference to put all of my customizations into a separate file.

1. In your project file directory, create a new file called Customized.targets (actually call it whatever you want).
2. Now open your .csproj in notepad++, copy the first and last lines and add them to Customized.targets. Should look something like this

<Project DefaultTargets=”Build” xmlns=”http://schemas.microsoft.com/developer/msbuild/2003&#8243; ToolsVersion=”3.5″>
</Project>

3. At the bottom of your .csproj add these two lines

<Import Project=”Customized.targets” />
<Import Project=”$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets” />

What is not optional is adding the Import of the MSBuild community tasks.

Step Two – Add Publish Template

  1. In your project file directory, add a new directory called Publish.
  2. Add this file to that directory.

You don’t really need to but I also added the directory to my project. At the very least you do have to add it to source control (otherwise it won’t get to the build machine)

Step Three – Customizations

Add these customizations to your Customized.targets file. Remove the numbers, they are just there for reference below…

1.	<PropertyGroup>
		<BuildEnvironment>DEV</BuildEnvironment>
	</PropertyGroup>

2.	<Choose>
		<When Condition=" '$(BuildEnvironment)' == 'DEV' ">
		  <PropertyGroup>
		    <PublishDir>\\MachineIP\ReleaseDirectory\$(BuildEnvironment)\</PublishDir>
		  </PropertyGroup>
		</When>
		<When Condition=" '$(BuildEnvironment)' == 'QA' ">
		  <PropertyGroup>
		    <PublishDir>\\MachineIP\ReleaseDirectory\$(BuildEnvironment)\</PublishDir>
		  </PropertyGroup>
		</When>	</Choose>

3.	<PropertyGroup>
		<!-- Note this must be done AFTER the above Choose (so PublishDir is set)-->
		<PublishFilePath>$(PublishDir)publish.html</PublishFilePath>
	</PropertyGroup>

4.	<ItemGroup>
		<Tokens Include="PublisherName">
		  <ReplacementValue>$(PublisherName)</ReplacementValue>
		  <Visible>false</Visible>
		</Tokens>
		<Tokens Include="ProductName">
		  <ReplacementValue>$(ProductName)</ReplacementValue>
		  <Visible>false</Visible>
		</Tokens>
		<Tokens Include="ApplicationVersion">
		  <ReplacementValue>$(ApplicationVersion)</ReplacementValue>
		  <Visible>false</Visible>
		</Tokens>
		<Tokens Include="Prerequsites">
		  <ReplacementValue>@(BootstrapperPackage->'&lt;li&gt;%(ProductName)&lt;/li&gt;','%0D%0A')</ReplacementValue>
		  <Visible>false</Visible>
		</Tokens>
		<Tokens Include="Username">
		  <ReplacementValue>$(Username)</ReplacementValue>
		  <Visible>false</Visible>
		</Tokens>
	</ItemGroup> 

5.	<Target Name="AfterPublish">
		<Time Format="dd/MM/yyyy HH:mm">
		  <Output TaskParameter="FormattedTime" PropertyName="PublishTime" />
		</Time>
		<!-- Finalise the publish.htm template file and copy it to the publish location -->
		<TemplateFile Template="Publish\publish.template.html" Tokens="@(Tokens)" OutputFilename="$(PublishFilePath)" />
		<FileUpdate Files="$(PublishFilePath)" Regex="\${PublishTime}" ReplacementText="$(PublishTime)" />
	</Target>

1. More on the BuildEnvironment property in a later posting. It’s not essential just remove all references to it if you want to remove it.

2. Also more on this in a later post, it is for specifying details for multiple environments. In the provided sample the PublishDir is the same machine and parent directory for both DEV and QA with a separating sub directory of the environment name. Don’t forget to SHARE the ReleaseDirectory folder!

3. Tells MSBuild where to put the publish.html file (name it what you want)

4. The publish.template.html has a number of tokens which need to be set.  Adjust these how you want, add or remove whatever tokens you want in the template.

5. After MSBuild finishes publishing it will run this section which creates the publish.html file using the token values.

Save those changes and you are done, you should now be able to run MSBuild, call publish and have a publish.html created for you with relevant information.

Step Four – Run MSBuild

Open Visual Studio command prompt, navigate to your project directory, execute (this assumes you only have one project file in the directory):

msbuild /t:Publish /p:Configuration=Release /p:BuildEnvironment=DEV

Done!

Why Bother?

If you get the file from VS why then care about what happens when publishing directly from msbuild?

I can think of three reasons why you need to publish from msbuild.

First, ideally developers should never publish from their machines.  (I say ideally because I have to raise a guilty hand and confess this is currently what I am doing…sometimes cirumstance forces bad practice upon us). This means ideally you have a build machine and it is from this machine only that releases should be done. So if you want to use VS to do the publish then you need to install and licence that machine.

Secondly, you want to be able to do a release from the command line, you don’t want to muck around starting VS, wait for the solution to load, open properties and click publish, what a waste of time, with msbuild you can create a batch file or two and start the publish process in the time it takes you to navigate to that directory.

Finally, the customization options available to you from within VS is too limiting. For example, if you are using VS to publish to multiple environments, how do you handle changing the parameters for each environment?

Some Notes

You can of course still run publish from VS but I don’t recommend it, get out of that bad habit and start using MSBuild directly. However if you insist on using VS, if you make any changes to Customization.targets, you need to unload and reload your project in VS for those changes to be ‘picked up’ by VS.

Please let me know if you identify any flaws or improvements that may be made with this approach.

<Choose>
<When Condition=” ‘$(BuildEnvironment)’ == ‘DEV’ “>
<PropertyGroup>
<BaseUrlPmsServices>https://ecsport3.cps.com.au/Pms</BaseUrlPmsServices&gt;
<BaseUrlEcsServices>https://ecsport3.cps.com.au/Ecs</BaseUrlEcsServices&gt;
<BaseUrlEdasServices>https://edasport.cps.com.au:1800/services</BaseUrlEdasServices&gt;
<BaseUrlHelp>http://ecstest.cps.com.au/PmsHelp</BaseUrlHelp&gt;
<PublishDir>\\172.16.101.101\ReleaseMark\$(BuildEnvironment)\</PublishDir><!– InstallUrl>It seems that installUrl doesn’t get picked up if set here, must be fed in with the msbuild command arguments.</InstallUrl –>
<!– ApplicationVersion is set in the publishENV.bat –>
</PropertyGroup>
</When>
<When Condition=” ‘$(BuildEnvironment)’ == ‘SUB’ “>
<PropertyGroup>
<BaseUrlPmsServices>https://ecspmsport1/Pms</BaseUrlPmsServices&gt;
<BaseUrlEcsServices>https://ecspmsport1/Ecs</BaseUrlEcsServices&gt;
<BaseUrlEdasServices>https://edas2-port1:1800/services</BaseUrlEdasServices&gt;
<BaseUrlHelp>http://ecspmshq/PmsHelp</BaseUrlHelp&gt;
<PublishDir>\\ecspmsport1\SmartClient\</PublishDir>
</PropertyGroup>
</When>
<When Condition=” ‘$(BuildEnvironment)’ == ‘DPS’ “>
<PropertyGroup>
<BaseUrlPmsServices>https://ecspmsport2/Pms</BaseUrlPmsServices&gt;
<BaseUrlEcsServices>https://ecspmsport2/Ecs</BaseUrlEcsServices&gt;
<BaseUrlEdasServices>https://edas2-port2:1800/services</BaseUrlEdasServices&gt;
<BaseUrlHelp>http://ecspmshq/PmsHelp</BaseUrlHelp&gt;
<PublishDir>\\ecspmsport2\SmartClient\</PublishDir>
</PropertyGroup>
</When>
</Choose>
    • Sameer
    • December 15th, 2009

    The publish template file seems to be inaccessible, could you provide an alternate location for the template?

      • wallism
      • December 18th, 2009

      Thanks Sameer, that link is to skydrive which is blocked by some companies…I’ve added a second link 🙂

    • Kin
    • January 18th, 2010

    I am absoutely new to trying to use MSBUILD to deploy a smart client app. One thing I don’t understand is where is the get latest action? Without the the get latest, the MSBUILD is building from the old source. If one needs to start VS in order to get latest, then MSBUILD is not as useful. Am I missing something?

      • wallism
      • January 19th, 2010

      To get the latest from MsBuild you can either create your own task or use community tasks.
      However, I see the ‘get latest’ action as part of an automated build process. So the automated build process (usually on a separate ‘build machine’), detects a change is source control, gets latest, builds everything, runs all tests then creates a new successful build if everything worked.
      It is from this successful build that you should deploy.
      In fact I would say that getting the latest version from source control should definately not be a ‘step’ in the deployment process, only deploy code that has been built and had all unit tests run.
      Check out my post on CI for more info on automating the build process. That posting makes use of NAnt for the build files but you could easily use MsBuild if that was your preference.

    • wallism
    • March 25th, 2010

    which file doesn’t get created? Just one or does the whole publish fail?

    Did you create the new customized.targets file, then add the code in the blog (customized with your details), plus add the ‘Import Porject” statements into your .csproj and then reload the project (or restart VS)?

    • Oleksandr Klymenko
    • April 27th, 2010

    Hi, Mark!
    Thanks for your post! It helped me greatly to understand what is needed to do. But as usual my concrete case have some differences from theory.
    I woul be very greatful to you if you can suggest how to do the same thing if:
    1) changes to .csproj file are impossible
    2) publish.htm generation needed only on automated builds from TFS.
    My exact problem is that I try to use MSBuild task to publish my csproj and can not get its inner properties such as ProductName, PublisherName and ApplicationRevision. First two I can hardcode, but what about ApplicationRevision and ApplicationVersion, needed for publish.htm? Is there a way to get these properties or I need to write a custom task for parsing .csproj file?
    Thanks in advance!

      • wallism
      • April 28th, 2010

      Second question first, the target that generates the publish.html file is the AfterPublish target, so it will only get generated when someone publishes the build…which should only be done using a ‘successful’ build, preferably from a build or deployment machine.

      If you cannot edit the csproj file then you are severely limted, I don’t know what you could do.

      So I assume you mean that you have created a custom MsBuild task which publishes your project(s)? If so I would suggest making use of your custom task from within your csproj.

      Just add a reference to your custom task dll at the top of the csproj.

      <UsingTask AssemblyFile=”..\..\..\MyDependencies\MyBuildTask\MyBuildTask.dll” TaskName=”MyBuildTask.MyPublishTask” />

      Then create a new target in the csproj (towards the bottom of the file….doesn’t matter too much as long as it is AFTER the properties you mentioned have been set) which ‘calls’ your custom task.

      <Target Name=PublishMyProject>
      <MyPublishTask SetThisProperty=”abc” />
      />
      Then call that target when running MsBuild from the cmd prompt:
      msbuild /t:PublishMyProject

      Does that kind of help??

      It does seem kind of odd having a completely separate MsBuild task. My understanding is that custom tasks should be used to do extra things you need to do as part of the build/publish process…not to control the process.

      Don’t forget to checkout msbuild community tasks as well as MsBuild Extension Pack.

        • Oleksandr Klymenko
        • April 28th, 2010

        Hi, Mark!
        Thank you very much for your answer!
        I realy can not edit csproj file. Some builds are made by developers on their owm machines and I can not add a reference to dll that is not installed on their machines. I know that it is not the best practice, but I came into team in the middle of the project and trying to improve builds step by step.
        However, I found a workaround! I made additional target in TFS buid definition, which publishes application to needed location, then using XmlRead task from msbuild community tasks (Thanks for recomendation!) I read PublisherName,ProductName and ApplicationVersion from csproj and create the publish.html from template, as it is done in your post. It works great!
        Thank you once again!

    • Raj
    • April 5th, 2011
    • Raj
    • April 8th, 2011

    Thanks for quick reply,

    I am able to sucessfully Publish the Deployment in to a RootFolder bin\Release\app.publish\Application I have few questions based on the Observation

    1. In Application Files Folder the new Published version is replacing the Older which is not in case of VS 2010 Publish i.e it makes sub folder for different Versions.

    2. Does Publish Version Increments Automatically?

    Thanks Again
    Raj

      • wallism
      • April 8th, 2011

      Hey Raj,

      The auto increment is something you need to do yourself, I never got round to it. If you’re using TFS I’d look at using that to handle the build number incrementing as part of your automated build.

      So what I was doing was manually providing the build number. If you give it a build number it should publish to a different directory (e.g. Build1.0.15.12).

      This is stretching my memory a little….good luck!

      Cheers,

      Mark

    • Geraldo
    • February 14th, 2013

    Hi Mark,

    Your template works like a charm! But I am facing an annoying little problem which I can’t solve. Hopefully you can help my out here.

    I have two Prerequisites in my projectfile of which two are not to be present on the publish.html page.

    False
    .NET Framework 3.5 SP1 Client Profile
    false

    False
    .NET Framework 3.5 SP1
    false

    The attributes Visible and Install are both false for the packages. When I publish from VS2010 these Prerequisites are not visible on the publish.html page. But when I build from TFS using your template they are visible.

    I have tried to remove them, but every time I change something in my projectsetting they magically reappear even though none of my projects reference them. Must be a special VS2010 feature ;-).
    I have also tried to exclude them in Customized.targets but I didn’t succeed.

    Is there a way to filter them out of the final result without removing them from the project page?

      • Geraldo
      • February 14th, 2013

      Should have known the xml wouldn’t go well in html 😉
      Another try:
      <BootstrapperPackage Include=”Microsoft.Net.Client.3.5″>
      <Visible>False</Visible>
      <ProductName>.NET Framework 3.5 SP1 Client Profile</ProductName>
      <Install>false</Install>
      </BootstrapperPackage>
      <BootstrapperPackage Include=”Microsoft.Net.Framework.3.5.SP1″>
      <Visible>False</Visible>
      <ProductName>.NET Framework 3.5 SP1</ProductName>
      <Install>false</Install>
      </BootstrapperPackage>

    • busybee
    • May 27th, 2015

    One doubt. After including the Customized.Targets .. if by chance you try to do a publish at the Visual Studio itself, Errors are generated stating>
    1.Unable to build.
    2. Unable to find “TemplateFile” task at Path ..\Publish\Publish.htm , etc

    • wallism
    • September 6th, 2015

    Fixed the link to the publish.html.
    Haven’t opened my blog for years, apologies for being so slow for fixing it 🙂

  1. December 22nd, 2009
  2. January 28th, 2010
  3. May 11th, 2010
  4. January 7th, 2011
  5. August 9th, 2011

Leave a reply to Raj Cancel reply