ClickOnce CAB Questions

Topics: CAB & Smart Client Software Factory
Mar 21, 2006 at 1:24 PM
originally posted by: justaddwater


I am now trying to deploy with Click Once, and have run into 2 problems/questions.

1) Since the default behavior of a module is to build into the shell /bin, I went back and added references to make Click Once happy. This works, but I wasn’t sure if this is the 'right' way to handle forcing Click Once to send out all the required dll’s.

2) I have a shell project, common project and 5 modules.

I think that with Click Once I should be able to make a change to module X and redeploy, in turn the user would only need to download that one assembly.
The problem I am having is, a change to 1 module forces the user to download the entire application again. I must just be missing something, what is the way around this?


Mar 22, 2006 at 8:20 AM
originally posted by: BradWilsonMSFT

1. This is clearly the easiest and least painful way to make it happen, if you want to use Visual Studio's ClickOnce deployment system.

2. I believe this is possible if you write custom ClickOnce handlers on the server. With the default Visutl Studio deployment system, you're going to end up re-deploying the entire application, not just the things that change from version to version.
Mar 22, 2006 at 9:18 AM
originally posted by: justaddwater


1) Thanks... We will stick with this style for now then.

2) I have this question posted on the MSDN click once/setup forum - the answer seems to be that the default behavior of click once is to only deploy the changes, but like your saying I don’t get that behavior... I will look into click once handlers, also I think mage may be the answer to this as well, Ill post what I find when/if I find the answer, since in my searches I have seen this or similar questions all over but no solid answer.

Mar 29, 2006 at 7:51 PM
originally posted by: MichaelBouck


<ItemGroup><br /> <SourceFiles Include="$(SourceDir)\*\." Exclude="$(SourceDir)\*\*.pdb"/><br /> </ItemGroup><br /> <ItemGroup><br /> <GetVersionAssembly Include="$(SourceDir)\$(SolutionName).exe"/><br /> </ItemGroup><br /> <ItemGroup><br /> <ClickOnceLandingPage Include="ClickOnce\ClickOnceLandingPage.htm"/><br /> </ItemGroup><br /> <ItemGroup><br /> <BootstrapperFile Include="Microsoft.Net.Framework.2.0"><br /> <ProductName>.NET Framework 2.0</ProductName><br /> </BootstrapperFile><br /> </ItemGroup><br /> <ItemGroup><br /> <ClickOnceInstallationFiles Include="$(SolutionName).application"/><br /> <ClickOnceInstallationFiles Include="$(SolutionName).exe.manifest"/><br /> <ClickOnceInstallationFiles Include="setup.exe"/><br /> <ClickOnceInstallationFiles Include="default.htm"/><br /> </ItemGroup><br /> <Target Name="Deploy" DependsOnTargets="PrepareClickOnceDeployment;DeleteVirtualRootFiles;CopyFilesToVirtualRoot"><br /> <OnError ExecuteTargets="NotifyDeployError"/><br /> </Target><br /> <Target Name="GetVersion"><br /> <Message Text="Getting version info..."/><br /> <GetAssemblyIdentity AssemblyFiles="@(GetVersionAssembly)"><br /> <Output TaskParameter="Assemblies"<br /> ItemName="GetVersionAssemblyInfo"/><br /> </GetAssemblyIdentity><br /> </Target><br /> <Target Name="GetPublishContent"><br /> <FolderClean Path="$(PublishDir)"/><br /> <ForceCopy SourceFiles="@(SourceFiles)"<br /> DestinationFiles="@(SourceFiles->'$(PublishDir)\%(RecursiveDir)%(Filename)%(Extension)')"/><br /> <ForceCopy SourceFiles="@(ClickOnceLandingPage)"<br /> DestinationFiles="@(ClickOnceLandingPage->'.\default.htm')"/><br /> </Target>

Mar 29, 2006 at 7:53 PM
originally posted by: MichaelBouck


<Target Name="PrepareClickOnceDeployment" DependsOnTargets="GetPublishContent;GetVersion"><br /> <Message Text="Preparing for ClickOnce deployment for version %(GetVersionAssemblyInfo.Version)..."/><br /> <CreateItem Include="$(PublishDir)\*\.*"><br /> <Output TaskParameter="Include" ItemName="ClickOncePublishFiles"/><br /> </CreateItem><br /> <ModifyFile<br /> Path="default.htm"<br /> RegularExpression="#APPLICATION_NAME#"<br /> NewValue="$(SolutionName)"<br /> Force="true"/><br /> <ModifyFile<br /> Path="default.htm"<br /> RegularExpression="#TITLE#"<br /> NewValue="$(ClickOnceAppTitle)"<br /> Force="true"/><br /> <ModifyFile<br /> Path="default.htm"<br /> RegularExpression="#VERSION#"<br /> NewValue="%(GetVersionAssemblyInfo.Version)"<br /> Force="true"/> <br /> <ProcessInvoker FileName="mage.exe"<br /> Arguments="-New Application -TrustLevel FullTrust -ToFile $(SolutionName).exe.manifest -Name &quot;$(ClickOnceAppTitle)&quot; -Version %(GetVersionAssemblyInfo.Version) -FromDirectory $(PublishDir)"/><br /> <ModifyFile<br /> Path="$(SolutionName).exe.manifest"<br /> RegularExpression="&lt;application /&gt;" <br /> NewValue="&lt;description asmv2:iconFile=&quot;$(SolutionName).ico&quot; xmlns=&quot;urn:schemas-microsoft-com:asm.v1&quot; /&gt;&lt;application /&gt;"<br /> Force="true"/>

Mar 29, 2006 at 7:56 PM
originally posted by: MichaelBouck


<ProcessInvoker FileName="mage.exe"<br /> Arguments="-Sign $(SolutionName).exe.manifest -CertFile $(SigningCert) -Password $(SigningCertPassword)"/><br /> <GenerateDeploymentManifest AssemblyName="$(ClickOnceAppTitle).application"<br /> AssemblyVersion="%(GetVersionAssemblyInfo.Version)"<br /> Description="My app description."<br /> Product="$(ClickOnceAppTitle)"<br /> Publisher="My Company"<br /> SupportUrl="$(SupportUrl)"<br /> EntryPoint="$(SolutionName).exe.manifest"<br /> Install="true"<br /> UpdateEnabled="true"<br /> UpdateMode="Foreground"<br /> OutputManifest="$(SolutionName).application"<br /> DeploymentUrl="$(ClickOnceApplicationUrl)"<br /> MapFileExtensions="true"/><br /> <ProcessInvoker FileName="mage.exe"<br /> Arguments="-Sign $(SolutionName).application -CertFile $(SigningCert) -Password $(SigningCertPassword)"/><br /> <GetFrameworkSdkPath><br /> <Output TaskParameter="Path" PropertyName="SdkPath" /><br /> </GetFrameworkSdkPath><br /> <GenerateBootstrapper <br /> ApplicationFile="$(SolutionName).application"<br /> ApplicationName="$(ClickOnceAppTitle)"<br /> ApplicationUrl="$(ClickOnceUrl)"<br /> BootstrapperItems="@(BootstrapperFile)"<br /> Culture="en"<br /> FallbackCulture="en-US"<br /> CopyComponents="true"<br /> Validate="false"<br /> Path="$(SdkPath)\Bootstrapper" <br /> OutputPath="." /><br /> </Target>

Mar 29, 2006 at 7:57 PM
originally posted by: MichaelBouck


<Target Name="DeleteVirtualRootFiles"><br /> <Message Text="Deleting files/directories from $(ClickOnceVirtualRootDir)..."/><br /> <FolderClean Path="$(ClickOnceVirtualRootDir)"/><br /> </Target><br /> <Target Name="CopyFilesToVirtualRoot" DependsOnTargets="PrepareClickOnceDeployment;DeleteVirtualRootFiles"><br /> <Message Text="Copying files to $(ClickOnceVirtualRootDir)..."/><br /> <ForceCopy SourceFiles="@(ClickOnceInstallationFiles)"<br /> DestinationFiles="@(ClickOnceInstallationFiles->'$(ClickOnceVirtualRootDir)\%(Filename)%(Extension)')"/><br /> <ForceCopy SourceFiles="@(ClickOncePublishFiles)"<br /> DestinationFiles="@(ClickOncePublishFiles->'$(ClickOnceVirtualRootDir)\%(RecursiveDir)%(Filename)%(Extension).deploy')"/><br /> </Target><br /> </Project>

Some things to note about the above:

- I'm targeting 3 different environments to publish the app to (Dev/QA/Prod). Depending on the environment you specify I pull-in the environment-specific artifacts into the publish folder that I generate the application manifest off of.
- I'm setting the ClickOnce version to the same version info that is stamped on my assemblies.
- I'm fixing-up the ClickOnce landing page with the target environment and version info (I used the VS.NET Publish functionality to gen the landing page which I then templatized...)
- I'm using the Microsoft Solutions Build framework ( for some tasks as well as a few custom tasks (no source but pretty obvious what these custom tasks do)

There are four ways to create a ClickOnce deploy package:
- Via the Window app's Publish property page in VS.NET (easiest but essentially unuseable for apps of even moderate complexity)
- Via mage.exe (command-line)
- Via mageui.exe (Windows app)
- Via msbuild (using the GenerateApplicationManifest, GenerateDeploymentManifest, GenerateBootstrapper and SignFile tasks).
Note that despite the chosen method, everything eventually calls the msbuild tasks. So, for the cleanest solution you should just cut straight to the chase and call the MSBuild tasks, right? Ah, but here's the rub -- you have to determine what your application manifest is (i.e. the list of artifacts that make up your app) and feed this list to GenerateApplicationManifest via an ItemGroup. You really don't want to have this fixed list that you have to maintain but rather have the application manifest dynamically generated for you. This is the real value of mage.exe which you can point to a directory and it will build-up the app manifest for you. Where mage is less than useful is generating the deployment manifest but you can just use the MSBuild task to do this instead.

Works like a charm!
Mar 29, 2006 at 8:09 PM
originally posted by: MichaelBouck

What you want in a CAB distro scenario is to keep the loose coupling of your modules (i.e. no module project references from your shell project). Unfortuneately, as you've seen, the "point-and-shoot" ClickOnce method in VS.NET walks the project references of the app to determine what the application manifest is. Fortuneately, I've got this working from the command-line and wired-in to an MSBuild deployment script (i.e. I'm not using VS.NET for the ClickOnce setup and do not have project references to my modules). Here's the gist:

&lt?xml version=&quot1.0&quot encoding=&quotutf-8&quot?&gt<br /> &ltProject DefaultTargets=&quotDeploy&quot xmlns=&quot<br /> &ltImport Project=&quotDeployCommon.proj&quot/&gt<br /> &ltImport Project=&quotMicrosoft.Sdc.Common.tasks&quot/&gt<br /> &ltPropertyGroup&gt<br /> &ltConfiguration Condition=&quot'$(Environment)' == ''&quot&gtDevelopment&lt/Configuration&gt<br /> &ltPublishDir&gtPublish&lt/PublishDir&gt<br /> &ltSolutionName&gtMyApp&lt/SolutionName&gt<br /> &ltSupportUrl&gt<br /> &ltSigningCert&gtClickOnce\MyCompany.pfx&lt/SigningCert&gt<br /> &ltSigningCertPassword&gtmycertpassword&lt/SigningCertPassword&gt<br /> &lt/PropertyGroup&gt<br /> &ltChoose&gt<br /> &ltWhen Condition=&quot '$(Environment)'=='Development' &quot&gt<br /> &ltPropertyGroup&gt<br /> &ltSourceDir&gtDebug&lt/SourceDir&gt<br /> &ltClickOnceVirtualRootDir&gt\\clickoncepublishmachine\c$\ClickOnce\$(SolutionName)&lt/ClickOnceVirtualRootDir&gt<br /> &ltClickOnceUrl&gthttp://clickoncepublishmachine:9090/$(SolutionName)/&lt/ClickOnceUrl&gt<br /> &ltClickOnceApplicationUrl&gt$(ClickOnceUrl)$(SolutionName).application&lt/ClickOnceApplicationUrl&gt<br /> &ltClickOnceAppTitle&gtMyApp (Dev)&lt/ClickOnceAppTitle&gt<br /> &lt/PropertyGroup&gt<br /> &lt/When&gt<br /> &ltWhen Condition=&quot '$(Environment)'=='QA' &quot&gt<br /> ...<br /> &lt/When&gt<br /> &ltWhen Condition=&quot '$(Environment)'=='Production' &quot&gt<br /> ...<br /> &lt/When&gt<br /> &lt/Choose&gt

Apr 2, 2006 at 10:45 PM
originally posted by: MichaelBouck

I've received a few requests to blog this solution (probably should have done this first rather than fight with the forums!) Here it is (along with a sample app no less!):,guid,d0a0dd1e-c9ac-4fa9-a408-615454d49702.aspx
Apr 3, 2006 at 11:24 AM
originally posted by: justaddwater

Thanks, this is great