What's New in Visual Studio 2008 SP1 ClickOnce Tooling

Here is a post I've been meaning to do for a while, but avoided because there is some MSDN info on it. But a recent question to the forums made me think that maybe it was time to talk about things that were not addressed in the document: what's really new in Visual Studio 2008 SP1 (at least in respect to ClickOnce tooling). But before we start talking about what's new, let's start talking about what's old.

One great thing about ClickOnce applications is that the deployment model is very declarative: simply by looking at an XML file, you can earn all sorts of things about the installation. Have you ever tried looking at an MSI file? It's pretty complicated. And that's not a knock: it's complicated because it has to be (okay, maybe a small knock). One of the items in the ClickOnce manifest are "pre-requisite assemblies", which are expressed as a preRequisite assembly dependency:

  <dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true"> 
    <assemblyIdentity name="ClassLibrary1" version="" publicKeyToken="F10848788BBE80F6" language="neutral" processorArchitecture="msil" /> 

This is stating that the application requires a specific strongly-named assembly (ClassLibrary1) in order to run. Especially noteworthy is the "dependencyType" attribute: a value of "preRequisite" means that the stated dependency must be available on the machine in order for the ClickOnce application to run. More succinctly, the assembly has to be installed to the global assembly cache. How to get the assembly into the cache is your problem.

Consider the following fragment, which I took out of an application manifest that targets the 2.0 framework. For the simple application I created, this is the only prerequisite assembly listed.

  <dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true"> 
    <assemblyIdentity name="Microsoft.Windows.CommonLanguageRuntime" version="2.0.50727.0" /> 

This is the one assembly dependency that breaks the rules. First of all, what is this assembly? There isn't anything in the GAC called Microsoft.Windows.CommonLanguageRuntime! Well, this particular assembly dependency is basically a code to the ClickOnce runtime engine saying that the 2.0 CLR needs to be installed in order to run the application (I won't bother to mention that this seems a little strange: since the ClickOnce runtime engine is reading the file, and the engine is only installed as part of the 2.0 Framework, it stands to reason the 2.0 CLR is there. Okay, I guess I will bother to mention it.).

Another thing to notice: why is this the only dependency listed? My application has a reference to System.dll and System.Windows.Forms.dll, so shouldn't these be listed as well? The answer: probably. But as part of the manifest generation the references that are redistributed with parts of the .NET Framework are stripped from the manifest. All of the references which are installed with the 2.0 framework are replaced with this one, because we take it on faith that if this one assembly is installed, all of the assemblies from that .NET Framework version are also installed. This one assembly has a special name: the "sentinel assembly".

When Visual Studio 2008 and its multi-targeting features came out, the sentinel assembly pattern was continued. When targeting the 3.0 Framework, the 3.0 assembly references are covered by the "WindowsBase" sentinel:

 <dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true"> 
  <assemblyIdentity name="WindowsBase" version="" publicKeyToken="31bf3856ad364e35" language="neutral" processorArchitecture="msil" /> 

Targeting the 3.5 framework used the "System.Core" sentinel:

 <dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true"> 
  <assemblyIdentity name="System.Core" version="" publicKeyToken="b77a5c561934e089" language="neutral" processorArchitecture="msil" /> 

The GenerateApplicationManifest task figures out which sentinel assembly to write based on a project property: the TargetFrameworkVersion property, which has an expected value of 2.0, 3.0, or 3.5. It does not use project references; within the project system, it's this framework version property which dictates the references that can be added to the appliction.

This brings us to Visual Studio 2008 SP1. When we first started planning for ClickOnce tooling for the service pack, it was obvious there would be the need for a new sentinel assembly. Once we heard the ginormous new set of features being added to the .NET Framework, we assumed there would be a whole new framework version. We were told there wouldn't be: the new feature set would be billed as a 3.5 "service pack", with the features being added into existing assemblies or entirely new assemblies.

Well, surely there will be a new "TargetFrameworkVersion" property the project system so that the manifest generation knows which set of sentinel assemblies to write into the manifest. Afraid not: the team that owned that property didn't have time to add a new option.

Okay, how about if we decide to always write out the 3.5 SP1 sentinel assembly? We were told that was fine: and there's even a good sentinel assembly to write out:

 <dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true"> 
  <assemblyIdentity name="System.Data.Entity" version="" publicKeyToken="b77a5c561934e089" language="neutral" processorArchitecture="msil" /> 

With that sentinel assembly decision out of the way, we decided to focus on adding tooling support for the 3.5 features that we missed, and were being added to the 3.5 SP1. These features are:

  • File Associations: I already wrote about this, but we added UI support for file associations.
  • Exclude deployment provider: When this options is checked, manifest generation will NOT include the deploymentProvider tag when it would under "normal" circumstances. This was supported in the VS 2008 product, but there was no UI for it.

We also added support for some (if not all) of the new ClickOnce features added for 3.5 SP1. Here's a rundown, including my commentary:

  • Suite Name: Places the start menu shortcut into a sub-folder. I think the goal of this one was to create a means for grouping similar applications into the same start menu folder, like Microsoft Office.
  • Error URL: Specifies the web page a user will be sent to if the installation of an application fails. My 2 cents: while I understand the ClickOnce team's heart was in the right place, I think this feature could have been executed better. At the very least, I would have liked to have seen 2 additions to this feature:
    1. A published web page template that would show reasonable help for some of the more common installation errors (something like the generated publish.htm, except about 1000 times more complicated)
    2. Including the application identity for the failed installation
  • Create desktop shortcut: Adds a shortcut to the desktop for the application that is being installed. A lot of people like this, a lot of people don't (I fall into the latter). But its nice to have the option.
  • Optional hashing: In the past, a file deployed with the application included a hash of the file when it was published. The hash was included for 2 reasons. Security: to make sure that the file that was published was consistent with the file that was downloaded. Updates: If ClickOnce is installing an update, downloading of a file would be skipped if the hash values for the old application and the new application were the same. Excluding a hash means that you can change a file post-publish. A potentially useful feature; I'm curious to hear if people modify files other than the .exe.config file.
  • Optional Signing: In the past, ClickOnce manifests needed to be signed in order to install. That restriction has been removed. Note that if a hash has been excluded, the manifests can not be signed. Publishing still defaults to signing the manifests, but once an application has been published, it is possible to turn off this feature (yes, this means you have to publish twice. We are sorry).

Of course, all of this required a new and improved publish options page: the old one was getting too crowded. The available properties were re-grouped into a format similar to what's in Tools: Options.

While the next set of features weren't added by the ClickOnce tooling team, they do affect ClickOnce published applications:

  • .NET Framework Client Profile: Publishing your Client Profile application adds additional information into the manifest to make sure that it can run on either the full 3.5 SP1 or the .NET Framework Client Profile. The profile includes all of the new 3.5 SP1 ClickOnce enhancements. I see how this profile might be a wonderful idea, but unfortunately additional tooling support is required to turn this into a nice experience.The good news: I'll probably get another blog post out of this.
  • Additional Bootstrapper Packages: Bootstrapper packages have been added for the full 3.5 SP1, the client profile, Visual Basic Power Packs (1.2), and Visual Studio Tools for Office. The bootstrapper for SQL Server Express 2008 is available when you install VB or C# Express.

Finally, some high-profile bug fixes were done:

  • Publishing WPF applications: In VS 2008, strange errors may crop up when publishing a WPF application: "...g.i.cs file can not be found".
  • Update to publish.htm: The javascript detection that is in place for detecting .NET Framework 2.0 of your application has been expanded to include .NET 3.0 and 3.5 SP1
  • XML Serializer assemblies are now published with the application: I wrote a blog post about this one, too. No need to use this work-around.

After all of this work was done, the tooling team was told that it was no longer acceptable to target 3.5 SP1 by default. Instead, when pulishing the application, the manifest generation tasks should be smart about this and only include the 3.5 SP1 sentinel assembly when it is "necessary". Unfortunately, because of how the service pack is constructed it is very difficult for manifest generation to know whether or not 3.5 SP1 is necessary. At manifest generation time, the relevant tasks deal only with project references, files, and ClickOnce properties. Manifest generation has no idea what a user is doing with those references, it only know the names of the references. And because 3.5 SP1 features were added to existing (2.0, 3.0, and 3.5) Framework assemblies, it isn't possible to base the sentinel addition on the referenced assembly list. This is why the a hack was settled upon: if your application requires 3.5 SP1, you should explicitly reference System.Data.Entity.dll. The manifest generation tool will understand what this means, and will add this sentinel assembly into the manifest file. Manifest generation will also (silently) add the 3.5 SP1 sentinel assembly if your application uses features only available in the 3.5 service pack.