Welcome to MSDN Blogs Sign in | Join | Help

How To: Generate publish.htm with MSBuild

One of the features Visual Studio provides when publishing is the generation of a web page that serves as an installation entry point of a ClickOnce application. It describes some information about the application, application requirements, and even includes a jscript that will bypass the bootstrapper if the required version of the .NET Framework is already installed. The web page is generated as part of the publishing phase (as opposed to the manifest generation phase) when the publish action is taken publishing within Visual Studio. Manifest generation is msbuild-based, while the publish phase is not.

There are several circumstances when you want thte web page created as part of the manifest generation process instead of the publish process. For example, when manifests are generated as part of an automated build process. Perhaps you want to use your own template for the web page, but still want some aspects of the page to be dynamically set. Or, matbe you want to translate the template into a different language, be it one available from a Visual Studio language pack, or one not supplied by Microsoft. Whatever your reason, this blog post will show you one way to get that done.

Background: How publish.htm is generated

Let's begin by looking at the template used to generate the web page. It is embedded as a resource in Microsoft.ViualStudio.Publish.dll, available in the GAC. I have attached it at the end of the post, but it is an XSL style sheet. Here's a little snippet:

<xsl:template  match="PageData"> 
 
  <HTML> 
 
    <HEAD> 
      <TITLE> 
        <xsl:value-of  select="ProductName"/> 
      </TITLE> 
      <META  HTTP-EQUIV="Content-Type"  CONTENT="text/html; charset=utf-8"/>

The stylesheet reads info out of an XML fragment, of which PageData is the root element. A scan of the stylesheet shows that the data used to transform the stylesheet may look something like this:

<PageData  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  xmlns:xsd="http://www.w3.org/2001/XMLSchema"> 
  <ApplicationVersion>1.0.0.0</ApplicationVersion> 
  <InstallUrl>ConsoleApplication.application</InstallUrl> 
  <ProductName>ConsoleApplication</ProductName> 
  <PublisherName>Mike Wade</PublisherName> 
  <SupportUrl>http://blogs.msdn.com/mwade</SupportUrl> 
  <Prerequisites> 
    <Prerequisite>.NET Framework 3.5</Prerequisite> 
    <Prerequisite>Windows Installer 3.1</Prerequisite> 
  </Prerequisites> 
  <RuntimeVersion>2.0.0</RuntimeVersion> 
  <BootstrapperText>The following prerequisites are required:</BootstrapperText> 
  <ButtonLabel>Install</ButtonLabel> 
  <BypassText>If these components are already installed, you can {launch} the application now. Otherwise, click the button below to install the prerequisites and run the application.</BypassText> 
  <HelpText>ClickOnce and .NET Framework Resources</HelpText> 
  <HelpUrl>http://msdn.microsoft.com/clickonce</HelpUrl> 
  <NameLabel>Name:</NameLabel> 
  <PublisherLabel>Publisher:</PublisherLabel> 
  <SupportText>Mike Wade Customer Support</SupportText> 
  <VersionLabel>Version:</VersionLabel> 
</PageData>

The xml input contains 3 basic types of information:

  1. Application specific information (publisher, product, install, etc.)
  2. Application prerequisite information (bootsrapper packages installed, a .NET Framework runtime used by the jscript)
  3. Static strings read from Microsoft.VisualStudio.Publish.dll

A build task to create publish.htm

Let's create a build task that will create the publish web page.
namespace CreatePublishWebPage 
{ 
    public class CreatePublishWebPage : Task 
    { 
    } 
}

This task will be performing the XML transform. Most of the work that the task does will be to populate the page data to get it ready for serialization and then used in the transform. The page data is declared like this:

public struct PageData 
{ 
    public string ApplicationVersion; 
    public string InstallUrl; 
    public string ProductName; 
    public string PublisherName; 
    public string SupportUrl; 
 
    [XmlArray()] 
    [XmlArrayItem("Prerequisite")] 
    public string[] Prerequisites; 
    public string RuntimeVersion; 
 
    public string BootstrapperText; 
    public string ButtonLabel; 
    public string BypassText; 
    public string HelpText; 
    public string HelpUrl; 
    public string NameLabel; 
    public string PublisherLabel; 
    public string SupportText; 
    public string VersionLabel; 
}

The first 5 items are all part of the deploy manifest. It would be possible to populate the information with the project properties used to build the manifest, or the information could be pulled directly from the manifest. Let's choose the latter. One slight difference is that the publisher and product are automatically populated when the manifest is generated, even if the properties are blank.

The prerequisites will be taken from the bootrapper build information, both the bootstraper package items as well as the bootstrapper enabled property. The RuntimeVersion data is only used when the jscript is written into the web page. There are 2 potential sources for this version: either by inspecting the list of bootstrapper packages, or by inspecting the application manifest's prerequisites. VisualStudio uses the former, but this task will use the latter: its a more accurate reflection of the true requirements of the application.

Finally, the other information will be part of the build tasks resources, which I also pulled out of Microsoft.VisualStudio.Publish.dll

With this in mind, it is easy to stub out the task, like this:

[Required] 
public string ApplicationManifestFileName { get; set; } 
 
[Required] 
public string DeploymentManifestFileName { get; set; } 
 
[Required] 
public bool BootstrapperEnabled { get; set; } 
 
public ITaskItem[] BootstrapperPackages { get; set; } 
 
public string TemplateFileName { get; set; } 
 
public string OutputFileName { get; set; } 
 
public override bool Execute() 
{ 
    PageData data = new PageData(); 
 
    WriteManifestInfo(ref data); 
    if (BootstrapperEnabled) 
    { 
        WriteBootstrapperInfo(ref data); 
    } 
    WriteStaticText(ref data); 
 
    XmlTransform(data); 
     
    return true; 
}

Now, let's get to th actual meat of the task, starting with WriteManifestInfo. The code will use the ManifestUtilities API to directly access the appropriate properties from DeployManifest. ManifestUtilities is defined in Microsoft.Build.Tasks.v3.5.dll, which unfortunately only exists in the GAC. You will have to copy it out of the GAC in order to add a reference to it. The code to populate the page data is not terribly tricky:

using Microsoft.Build.Tasks.Deployment.ManifestUtilities;

private DeployManifest deployManifest = null; 
private DeployManifest DeployManifest 
{ 
    get 
    { 
        if (deployManifest == null) 
        { 
            deployManifest = (DeployManifest)ManifestReader.ReadManifest(this.DeploymentManifestFileName, true); 
        } 
        return deployManifest; 
    } 
} 
 
 private void WriteManifestInfo(ref PageData data) 
{ 
    data.ApplicationVersion = DeployManifest.AssemblyIdentity.Version; 
    data.InstallUrl = Path.GetFileName(DeployManifest.SourcePath); 
    data.ProductName = DeployManifest.Product; 
    data.PublisherName = DeployManifest.Publisher; 
    data.SupportUrl = DeployManifest.SupportUrl; 
}

The code uses a simple accessor to access the DeployManifest, and then writes appropriate information to the page data.

Dealing with prerequisites is a little more complicated in. There are 3 important steps in the process:

  1. Add a list of all installed prerequisites into the Prerequisites field
  2. Determine if the only prerequisites are .NET Frameowrk (or Windows Installer 3.1) only
  3. Determine the required framework version based on the presence of specific .NET assemblies in the application manifest

For this last point, the WindowsBase prereq corresponds to the 3.0 framework, while System.Core corresponds to the 3.5 framework. The prereqs are looked by examining the AssemblyReferences of the application manifest. Now for the actual code:

using Microsoft.Build.Tasks.Deployment.Bootstrapper;

private static readonly string[] DOTNET30AssemblyIdentity = { "WindowsBase", "3.0.0.0", "31bf3856ad364e35", "neutral", "msil" }; 
private static readonly string[] DOTNET35AssemblyIdentity = { "System.Core", "3.5.0.0", "b77a5c561934e089", "neutral", "msil" }; 
private const string METADATA_INSTALL = "Install"; 
private const string PRODUCT_CODE_DOTNET_PREFIX = "Microsoft.Net.Framework."; 
private const string PRODUCT_CODE_WINDOWS_INSTALLER = "Microsoft.Windows.Installer.3.1";

private void WriteBootstrapperInfo(ref PageData data) 
{ 
    BootstrapperBuilder builder = new BootstrapperBuilder(); 
    ProductCollection products = builder.Products; 
     
    bool isDotNetOnly = true; 
    List<string> productNames = new List<string>(); 
    foreach (ITaskItem bootstrapperPackage in this.BootstrapperPackages) 
    { 
        string productCode = bootstrapperPackage.ItemSpec; 
        bool install = false; 
        bool.TryParse(bootstrapperPackage.GetMetadata(METADATA_INSTALL), out install); 
        if (install) 
        { 
            string productName = productCode; 
            Product product = products.Product(productCode); 
            if (product != null) 
            { 
                productName = product.Name; 
            } 
            productNames.Add(productName); 
 
            if (!productCode.StartsWith(PRODUCT_CODE_DOTNET_PREFIX, StringComparison.OrdinalIgnoreCase) && 
                !productCode.Equals(PRODUCT_CODE_WINDOWS_INSTALLER, StringComparison.OrdinalIgnoreCase)) 
            { 
                isDotNetOnly = false; 
            } 
        } 
    } 
 
    data.Prerequisites = productNames.ToArray(); 
    if (data.Prerequisites.Length > 0 && isDotNetOnly) 
    { 
        data.RuntimeVersion = GetRuntimeVersion(); 
    } 
} 
 
private string GetRuntimeVersion() 
{ 
    string version = "2.0.0"; 
    AssemblyReference dotNet30AssemblyReference = ApplicationManifest.AssemblyReferences.Find(CreateAssemblyIdentity(DOTNET30AssemblyIdentity)); 
    if (dotNet30AssemblyReference != null && dotNet30AssemblyReference.IsPrerequisite) 
    { 
        version = "3.0.0"; 
    } 
    AssemblyReference dotNet35AssemblyReference = ApplicationManifest.AssemblyReferences.Find(CreateAssemblyIdentity(DOTNET35AssemblyIdentity)); 
    if (dotNet35AssemblyReference != null && dotNet35AssemblyReference.IsPrerequisite) 
    { 
        version = "3.5.0"; 
    } 
 
    return version; 
} 
 
private AssemblyIdentity CreateAssemblyIdentity(string[] info) 
{ 
    return new AssemblyIdentity(info[0], info[1], info[2], info[3], info[4]);  
}

Getting the static text is not terribly exciting. There are only two things of note: the text for the "go" button changes depending on whether the application is installed or is online only, and the support information also incorporates the publisher name.

private void WriteStaticText(ref PageData data) 
{ 
    data.BootstrapperText = Properties.Resources.BootstrapperText; 
    if (deployManifest.Install) 
    { 
        data.ButtonLabel = Properties.Resources.InstallButtonLabel; 
    } 
    else 
    { 
        data.ButtonLabel = Properties.Resources.RunButtonLabel; 
    } 
    data.BypassText = Properties.Resources.BypassText; 
    data.HelpText = Properties.Resources.HelpText; 
    data.HelpUrl = Properties.Resources.HelpUrl; 
    data.NameLabel = Properties.Resources.NameLabel; 
    data.PublisherLabel = Properties.Resources.PublisherLabel; 
    data.SupportText = string.Format(CultureInfo.CurrentCulture, Properties.Resources.SupportText, DeployManifest.Publisher); 
    data.VersionLabel = Properties.Resources.VersionLabel; 
}

The last item of note is actually performing the transform, based on either a supplied template or using the default one (read from the task assembly's resources). The input is created by serializing the page data.

private void XmlTransform(PageData data) 
{ 
    if (!string.IsNullOrEmpty(this.OutputFileName)) 
    { 
        // Serialize all of the web page data 
        XmlSerializer serializer = new XmlSerializer(data.GetType()); 
        MemoryStream dataStream = new MemoryStream(); 
        StreamWriter streamWriter = new StreamWriter(dataStream); 
        serializer.Serialize(streamWriter, data); 
        dataStream.Position = 0; 
        XmlReader dataReader = XmlReader.Create(dataStream); 
 
        // Set up the transform 
        XmlDocument stylesheet = new XmlDocument(); 
        if (!string.IsNullOrEmpty(TemplateFileName)) 
        { 
            stylesheet.Load(TemplateFileName); 
        } 
        else 
        { 
            stylesheet.LoadXml(Properties.Resources.PublishPage); 
        } 
        XslCompiledTransform transform = new XslCompiledTransform(); 
        transform.Load(stylesheet); 
 
        // Perform the transform, writing to the output file 
        using (XmlTextWriter outputFile = new XmlTextWriter(this.OutputFileName, Encoding.UTF8)) 
        { 
            outputFile.Formatting = Formatting.Indented; 
            transform.Transform(dataReader, outputFile); 
        } 
    } 
}

Using the CreatePublishWebPage Task

To use the task, you need only to modify your project file, overriding the "AfterPublish" target:
<UsingTask  TaskName="CreatePublishWebPage"  AssemblyFile="$(MSBuildExtensionsPath)\\CreatePublishWebPage.dll" /> 
<Target  Name="AfterPublish"> 
  <CreatePublishWebPage  ApplicationManifestFileName="$(_DeploymentApplicationDir)$(_DeploymentTargetApplicationManifestFileName)"  
                          
                        DeploymentManifestFileName="$(PublishDir)$(TargetDeployManifestFileName)"  
                          
                        BootstrapperEnabled="$(BootstrapperEnabled)"  
                          
                        BootstrapperPackages="@(BootstrapperPackage)"  
                          
                        OutputFileName="$(PublishDir)$(WebPage)" /> 
</Target>

The above snippet writes the publish.htm to the app.publish folder under your config's output (e.g. bin\debug\app.publish). The relevant paths were pulled from the Microsoft.common.targets file. It may make sense to add a condition or 2 to the target, taking into account whether or not the build is happening from within Visual Studio (BuildingInsideVisualStudio) as well as whether or not the user wants to generate a publish.htm file (CreateWebPageOnPublish). Unfortunately, it appears the file is not picked up as part of the publish phase, so you may have to transport the file to its final resting place yourself.

I have attached the full project as a to the end of this post.

A note about Visual Studio 2008 SP1

There was a little bit of change around the web page, specifically the jscript used to detect the framework. I believe the SP behavior around the .NET Framework versions was similar to what was done here, in that the jscript could actually check for 3.0, 3.5, or 3.5 SP1. It seems that the Visual Studio 2008 RTM could only deal with 2.0 of the Framework. The SP also introduced the client version of the .NET Framework. While the jscript code was updated to take that into account, the PageData is not able to properly detect that the cllient framework bootstrapper package is being used, because of the ProductName used by the client bootststrapper package. Unfortunately, I can't remember for sure, and of course the SP 1 install from MSDN doesn't seem to work on my laptop.

Posted by mwade | 3 Comments

Attachment(s): CreatePublishWebPage.zip

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:

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

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.

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

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="3.0.0.0" publicKeyToken="31bf3856ad364e35" language="neutral" processorArchitecture="msil" /> 
</dependentAssembly>

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

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

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="3.5.0.0" publicKeyToken="b77a5c561934e089" language="neutral" processorArchitecture="msil" /> 
</dependentAssembly>

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.

Posted by mwade | 2 Comments

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.

Posted by mwade | 2 Comments

How To: Include XmlSerializers in your ClickOnce Application

Have you ever noticed that when your application has a web reference, and you go to publish that web reference, the expected XmlSerializers assembly is not written into the application manifest, nor is it deployed with the application? You haven't? Not too many people have: it rarely comes up in the forums I scan, but we have received a couple of support calls about this. The answers I have given in the past include things like:

  • Expose the web service methods through a partner Class Library, and take a reference on that Class Library
  • Make some modifications to Microsoft.Common.targets

In fact there is an easier way, which is similar to my last blog post. By adding the following information to your project file, the XmlSerializers assembly will start getting published with your application:

<Target Name="BeforePublish"> 
  <CreateItem Include="%(SerializationAssembly.FullPath)" Condition="'%(Extension)' == '.dll'"> 
    <Output TaskParameter="Include" ItemName="_SGenDllsRelatedToCurrentDll" /> 
  </CreateItem> 
</Target>

Once again, BeforePublish is pre-populating some items to include some information for the ClickOnce manifest generation. In this case, the _SGenDllsRelatedToCurrentDll items are used by the ResolveManifestFiles task to figure out what should be published. The definition of SGenDllsRelatedToCurrentDll in Microsoft.Common.targets only picks up on SGen Dlls from referenced projects, but misses the XmlSerializers from the exe project (which is why the first work-around mentioned above will work). Making this change allows the XmlSerializers to be picked up, and you can even set properties on them in the Application Files dialog.

Note: This problem existed in both VS 2005 and VS 2008. This work-around doesn't work for VS 2005 (so far, I haven't been able to come up with something that didn't require modifying Microsoft.Common.targets (or re-defining GenerateApplicationManifest target)). Furthermore, this issue is fixed in VS 2008 SP1 (to be precise: it is fixed in .NET Framework 3.5 SP1). The fix taken in there is basically what I showed here: Including the SerializationAssembly in the _SGenDllsRelatedToCurrentDll items.

Posted by mwade | 2 Comments

How To: Publish Files Which are not in the Project

Every one in a while on our ClickOnce forums, we are asked how to publish files that aren't part of the project. The short answer is: you can't. The ClickOnce project is only capable of publishing files (build output, data, or content files) which are part of the project (or referenced by the project). If you have a file in a different project that you want to add to your publishing project, you can't simply add that item to your ClickOnce project: the project system will copy the file to the ClickOnce project directory. This means that any change you make to the file in the original project will be lost in the ClickOnce project, unless you remember to make the corresponding change in your ClickOnce project. It's possible to better manage this mess by adding the item as a link; that helps a little, but you still have to add all of the files you want to publish into the project.

This wouldn't be an issue if the ClickOnce project were able to deal with Project Output Groups like a setup project can. Hopefully I will get to that in a later post, but for now let's keep it a little more simple: is there some way to include all of the files in a given directory in the ClickOnce publish? This is, after all, how mage operates. The answer is "yes", if you are willing to make some by-hand modifications to a project file. I won't be dealing with assemblies in this post; the need to do so isn't as pressing: I think it's more rare for an application to require a lot of assemblies as references that aren't adequately addressed with the current system for dealing with references. Indeed, you can think of project-to-project references as similar to project output groups, and other references match the "link" analogy, as references are not copied locally and then dealt with statically by the project system. Instead, I want to focus on how those assemblies which may require their own content files can make sure those are deployed with the ClickOnce application.

Let's get started by exploring how information from the application files dialog makes the journey to what gets written into the application manifest. For those of you who want to play along at home, I created a solution containing a Windows Forms Application and 2 class library projects. I also added 5 text files to a sub-folder of the WinForms project, one of which was added as a link. I changed a bunch of settings in the application files dialog, like this:

Application Files Dialog

Viewing the contents of the project file, we see this:

<ItemGroup> 
  <PublishFile Include="ClassLibrary1"> 
    <Visible>False</Visible> 
    <Group></Group> 
    <PublishState>Include</PublishState> 
    <TargetPath></TargetPath> 
    <FileType>Assembly</FileType> 
  </PublishFile> 
  <PublishFile Include="ClassLibrary2"> 
    <Visible>False</Visible> 
    <Group></Group> 
    <PublishState>Prerequisite</PublishState> 
    <TargetPath></TargetPath> 
    <FileType>Assembly</FileType> 
  </PublishFile> 
  <PublishFile Include="NewFolder1\\TextFile2.txt"> 
    <Visible>False</Visible> 
    <Group>Foo</Group> 
    <PublishState>Auto</PublishState> 
    <TargetPath></TargetPath> 
    <FileType>File</FileType> 
  </PublishFile> 
  <PublishFile Include="NewFolder1\\TextFile3.txt"> 
    <Visible>False</Visible> 
    <Group></Group> 
    <PublishState>Exclude</PublishState> 
    <TargetPath></TargetPath> 
    <FileType>File</FileType> 
  </PublishFile> 
  <PublishFile Include="NewFolder1\\TextFile4.txt"> 
    <Visible>False</Visible> 
    <Group>Foo</Group> 
    <PublishState>DataFile</PublishState> 
    <TargetPath></TargetPath> 
    <FileType>File</FileType> 
  </PublishFile> 
  <PublishFile Include="NewFolder1\\TextFileLink.txt"> 
    <Visible>False</Visible> 
    <Group></Group> 
    <PublishState>Include</PublishState> 
    <TargetPath></TargetPath> 
    <FileType>File</FileType> 
  </PublishFile> 
</ItemGroup>

All sorts of observations can be made:

  • Application File items are stored as Items with name "PublishFile"
  • The Include value is the name of the file in the application files dialog
  • If a metadata value is in the default state, it is generally left blank
  • If the entire item is in a default state, there is no entry for it in the project file (see TextFile1.txt for example)
  • There are 5 different metadata values for the PublishFile item
    1. Visible, which determines whether or not the Item appears in the solution explorer (always "False" because all of these have different representations within the project file)
    2. Group, which corresponds to the Download Group in the application files dialog
    3. PublishState, which corresponds to how the file is distributed. There appear to be 5 different values
      • (Auto): The file is in its default state
      • Include: The file will be published with the project
      • Exclude: The file will NOT be published with the project
      • DataFile: The file will be published as a data file
      • Prerequisite: The assembly will not be published with the application, but will be written into the manifest as a prerequisite
    4. TargetPath, which corresponds to the location relative to the application manifest the file will be published to (this is value is not settable through the application files dialog)
    5. FileType, which describes what kind of file this is
      • Assembly
      • File

Now that we have some idea of how files are included in output, let's see how they are applied by MSBuild. To do that, we start by opening Microsoft.Common.targets. Performing a search for @(PublishFile), we see the following entry:

<!-- Create list of items for manifest generation --> 
<ResolveManifestFiles 
                EntryPoint="@(_DeploymentManifestEntryPoint)" 
                ExtraFiles="@(_DebugSymbolsIntermediatePath);$(IntermediateOutputPath)$(TargetName).xml;@(_ReferenceRelatedPaths)" 
                Files="@(ContentWithTargetPath);@(_DeploymentManifestIconFile);@(AppConfigWithTargetPath)" 
                ManagedAssemblies="@(_DeploymentReferencePaths);@(ReferenceDependencyPaths);@(_SGenDllsRelatedToCurrentDll)" 
                NativeAssemblies="@(NativeReferenceFile);@(_DeploymentNativePrerequisite)" 
                PublishFiles="@(PublishFile)" 
                SatelliteAssemblies="@(IntermediateSatelliteAssembliesWithTargetPath);@(ReferenceSatellitePaths)" 
                TargetCulture="$(TargetCulture)"> 
 
    <Output TaskParameter="OutputAssemblies"  ItemName="_DeploymentManifestDependencies"/> 
    <Output TaskParameter="OutputFiles"  ItemName="_DeploymentManifestFiles"/> 
 
</ResolveManifestFiles>

This ResolveManifestFiles task takes all sorts of information (including PublishFile items). It returns the OutputAssemblies and OutputFiles as _DeploymentManifestDependencies and _DeploymentManifestFiles items, respectively. These items are then used by the GenerateApplicationManifest and a pair of Copy tasks within the _CopyFilesToPublishFolder target. Before we latch onto these values to publish files not in the project, let's see what sort of metadata the items carry along.

You could write your own task to figure this out, or you could take my word for it: here is the relevant metadata names for _DeploymentManifestFiles:

  • Group
  • TargetPath
  • IsDataFile

It's a similar group to what was written for PublishFile items. It appears that (for files) PublishState was used mapped to IsDataFile by the ResolveManifestFiles task. The FileType metadata was used used to break all PublishFile items into the assemblies and files group.

Let's check out the metadata values from these names. Let's modify the winform project file to display all of this metadata. Unload the project within Visual Studio and edit it with the following info:

<Target Name="AfterPublish"> 
  <Message Text="_DeploymentManifestFiles (Identity=%(_DeploymentManifestFiles.Identity))
\tab Group=%(_DeploymentManifestFiles.Group)
\tab TargetPath=%(_DeploymentManifestFiles.TargetPath)
\tab IsDataFile=%(_DeploymentManifestFiles.IsDataFile)" /> 
</Target> 
</Project>

After publishing, the output window shows (amongst other things):

_DeploymentManifestFiles (Identity=..\..\..\..\TextFileLink.txt)
    	Group=
    	TargetPath=NewFolder1\TextFileLink.txt
    	IsDataFile=false
_DeploymentManifestFiles (Identity=NewFolder1\TextFile1.txt)
    	Group=
    	TargetPath=NewFolder1\TextFile1.txt
    	IsDataFile=false
_DeploymentManifestFiles (Identity=NewFolder1\TextFile2.txt)
   	Group=Foo
    	TargetPath=NewFolder1\TextFile2.txt
    	IsDataFile=false
_DeploymentManifestFiles (Identity=NewFolder1\TextFile4.txt)
    	Group=
    	TargetPath=NewFolder1\TextFile4.txt
    	IsDataFile=true

So to add additional files to our output, we need only figure out how to generate new _DeploymentManifestFiles items. It turns out this is easy enough. Consider the following piece of information written into the project file:

<ItemGroup> 
  <AdditionalPublishFile Include="C:\\Documents and Settings\\mwade\\My Documents\\My Pictures\\*.jpg"> 
    <Visible>False</Visible> 
  </AdditionalPublishFile> 
</ItemGroup> 
<Target Name="BeforePublish"> 
  <Touch Files="@(IntermediateAssembly)" /> 
  <CreateItem Include="@(AdditionalPublishFile)" AdditionalMetadata="TargetPath=%(FileName)%(Extension);IsDataFile=false"> 
    <Output TaskParameter="Include" ItemName="_DeploymentManifestFiles" /> 
  </CreateItem> 
</Target>

These statements take all of the jpg files in my My Pictures directory, stores each item in a AdditionalPublishFile. Then as part of BeforePublish target, the PublishDirectoryFile are used (via the CreateItem task) to pre-populate the _DeploymentManifestFiles items. The CreateItem task sets the necessary metadata for _DeploymentManifestFiles: TargetPath, Group, and IsDataFile. The Touch task is used to make sure that GenerateApplicationManifest task is re-run as part of the publish process. Touch modifies the write time of the primary executable. This file is an input to the GenerateApplicationManifest task, and with a write time later than the write time of the application manifest (the task output), MSBuild will not skip the task. Publishing the project shows that these files are indeed written into the application manifest. Furthermore, the publishing service makes sure these files are published to the remote server. You can use your own MSBuild magic to include which ever files you want to include.

One thing to note: I have only tested this on VS 2008, and I don't know how well things will work on Visual Studio 2005. My expectation is that the manifest generation should be basically the same; however, its possible the files will not be automatically published with the application.

One final note: strictly speaking, MSBuild guidelines indicate that things which begin with "_" are supposed to indicate that the things are not for public consumption. So, we could take the approach of re-defining the Publish tasks to enable this scenario (and we could get that to work), but that would involve re-defining targets which are also considered "private" because they start with "_" as well. So either way, we're doing something we shouldn't; taking this approach will definitely be the easiest route.

Update 7/2/2008: Added information regarding the Touch task.
Posted by mwade | 2 Comments

How To: Use Macros to Configure Publish Settings

There are all sorts of publishing properties available to the Visual Studio developer. Visual Studio tries to set the properties to reasonable default values. But with all things that are defaulted, the values are right for some people, but wrong for others. In my day-to-day work, where I run through publishing quite a bit, there are quite a few values I want to reset. Its possible to click through the UI to change the values, but who has a couple of seconds to spare to open dialogs and click on stuff? Not I; every second lost is a second I could spend writing a blog entry. Fortunately, its easy to create a macro that will set these values for you. In this post, I'll write about you can use a macro to set these default values through the DTE.

Displaying publish properties and values

Let's start by viewing all available publishing properties. Before going too far, let's show some helper functions to accomplish our tasks. Here is some code I copied from my macro explorer:

Function GetPublishProperties() As EnvDTE.Properties
    Dim proj As Project = DTE.Solution.Projects.Item(1)
    If proj Is Nothing Then
        ShowError("Unable to retrieve a project", "GetPublishProperties")
        Return Nothing
    End If

    Dim publishProperty As EnvDTE.Property = proj.Properties.Item("Publish")
    If publishProperty IsNot Nothing Then
        Dim publishProperties As EnvDTE.Properties = TryCast(publishProperty.Value, EnvDTE.Properties)
        Return publishProperties
    End If

    ShowError("Unable to get publish properties of project " & proj.Name, "GetPublishProperties")
    Return Nothing
End Function

Sub ShowError(ByVal message As String, ByVal title As String)
    MsgBox(message, MsgBoxStyle.Exclamation Or MsgBoxStyle.OkOnly, title)
End Sub

Sub ShowException(ByVal ex As Exception, ByVal methodName As String)
    Dim message As String = ex.Message
    If ex.InnerException IsNot Nothing Then
        message = ex.InnerException.Message
    End If
    ShowError(message, methodName)
End Sub

GetPublishProperties is a function to retrieve the object which contains the publish properties. It grabs the first project in the solution, and selects the "Publish" property from there. Next, the resultant property value is converted to another properties container. Some error handling is also included (although probably not enough).

With that as a baseline, this macro will show all of the publish properties, types, and values:

Sub ShowPublishProperties()
    Dim publishProperties As EnvDTE.Properties = GetPublishProperties()
    If publishProperties IsNot Nothing Then
        Dim sb As New System.Text.StringBuilder()
        For Each prop As EnvDTE.Property In publishProperties
            sb.Append(String.Format("{0} [{1}]: {2}", prop.Name, prop.Value.GetType().ToString(), prop.Value.ToString()))
            sb.Append(vbCrLf)
        Next
        MsgBox(sb.ToString())
    End If
End Sub

This macro simply iterates through all of the publish properties, gathers the property names, types, and values and show them all in a message box. Here is a subset of the output:

PublisherName [System.String]: 
OpenBrowserOnPublish [System.Boolean]: True
BootstrapperComponentsLocation [System.Int32]: 0
PublishFiles [System.__ComObject]: System.__ComObject

Most of the values are strings, ints, or booleans. If you were to look in the project file, you would see that these values correspond to properties within the file. The exception are PublishFiles and BootstrapperPackages, which correspond to the Items in the project file, as well as the the lists within the Application Files dialog and Prerequisites dialog. These will be explored further in a bit. But first, let's try setting some easy values.

Setting Property values

Setting a value is as easy as reading a value. For example, to set the PublisherName property, one could write a macro like this:

Sub SetPublisherName()
    Dim publishProperties As EnvDTE.Properties = GetPublishProperties()
    If publishProperties IsNot Nothing Then
        Dim publisherNameProperty As EnvDTE.Property = publishProperties.Item("PublisherName")
        publisherNameProperty.Value = "Test Value"
    End If
End Sub

Some properties perform validation when the values are set. For example, the PublishUrl:

Sub SetPublishUrl()
    Dim publishProperties As EnvDTE.Properties = GetPublishProperties()
    If publishProperties IsNot Nothing Then
        Try
            publishProperties.Item("PublishUrl").Value = ""
        Catch ex As Exception
            ShowException(ex, "SetPublishUrl")
        End Try
    End If
End Sub

In the above example, an attempt is made to blank out the PublishUrl property. However, the PublishUrl can not be set to an empty string, as the thrown exception indicates:

An empty string is not allowed for property 'Publish Location'.

The thrown message uses "Publish Location" because that is what the label for the text box which corresponds to this property uses. In fact, the property page is pretty much setting this property in the exact same manner as the macro.

Modifying Items

There are 2 different types of Items exposed by the Publish

Properties: PublishFiles and BootstrapperPackages. Here is a macro that does something with PublishFiles:
Sub ListFiles()
    Dim publishProperties As EnvDTE.Properties = GetPublishProperties()
    If publishProperties IsNot Nothing Then

        Dim filesObject As Object = publishProperties.Item("PublishFiles")
        If filesObject Is Nothing Then
             ShowError("Could not get PublishFiles object", "ListFiles")
            Return
        End If

        Try
            Dim numFiles As Integer
            numFiles = filesObject.Value.Item("Count").Value
            Dim sb As New System.Text.StringBuilder()
            For i = 0 To numFiles - 1
                'display file name and publish status.
                sb.AppendLine("File Name=" & filesObject.Object.Item(i).Name & ", Status=" & filesObject.Object.Item(i).PublishStatus)
            Next
            MsgBox(sb.ToString(), MsgBoxStyle.DefaultButton1, "Application Files")
        Catch ex As Exception
            ShowException(ex, "ListFiles")
        End Try
    End If
End Sub

This macro relies on latebinding in doing its work: filesObject is declared as Object, yet it uses properties from that object. The filesObject.Object contains an accessor to a collection; there are actually 2 ways to get a value out of the collection: one could use either a string to get a file by name, or an index, like what was done above. Here is some sample output from running this macro:

File Name=WindowsApplication2.exe, Status=0 
File Name=WindowsApplication2.pdb, Status=0 
File Name=WindowsApplication2.xml, Status=0 

The "Status" of the file corresponds to the Publish Status column in the Application Files dialog. 0 corresponds to the "(Auto)" value.

Accessing the bootstrapper packages is similar:

Sub IncludeAllPrerequisites()
    Dim publishProperties As EnvDTE.Properties = GetPublishProperties()
    Dim bootstrapperPackages As Object = publishProperties.Item("BootstrapperPackages")
    If bootstrapperPackages IsNot Nothing Then
        Dim numPackages As Integer = bootstrapperPackages.Value.Item("Count").Value
        For i As Integer = 0 To numPackages - 1
            bootstrapperPackages.Object.Item(i).Install = True
        Next
    End If
End Sub

Sub ExcludeWindowsInstaller31()
    Dim publishProperties As EnvDTE.Properties = GetPublishProperties()
    Dim bootstrapperPackages As Object = publishProperties.Item("BootstrapperPackages")
    If bootstrapperPackages IsNot Nothing Then
        Dim windowsInstaller31Package As Object = bootstrapperPackages.Object.Item("Microsoft.Windows.Installer.3.1")
        If windowsInstaller31Package IsNot Nothing Then
            windowsInstaller31Package.Install = False
        End If
    End If
End Sub
The first macro includes all available prerequisites when publishing. In the first example, the packages are accessed by index. The second example gets a bootstrapper package by using a specific product code.

Publishing via a macro

Finally, just like it is possible to build from a macro, it is possible to publish as well. Here is an example that does something similar to what the Publish Now button does on the Publish property page:

Sub Publish()
    Dim proj As Project = DTE.Solution.Projects.Item(1)
    Dim sb2 As EnvDTE80.SolutionBuild2 = CType(DTE.Solution.SolutionBuild, EnvDTE80.SolutionBuild2)
    Dim config2 = CType(sb2.ActiveConfiguration, EnvDTE80.SolutionConfiguration2)
    Dim configName = String.Format("{0}|{1}", config2.Name, config2.PlatformName)
    sb2.BuildProject(configName, proj.UniqueName, True)
    sb2.PublishProject(configName, proj.UniqueName, True)
End Sub
Ideally, the macro should verify that the build succeeded before publishing, but that has been left as an exercise for the reader.
Posted by mwade | 1 Comments

Tool: Editing File Associations for ClickOnce Applications

I apologize for taking so long between posts. I have an excuse: very busy working on Visual Studio 2008 SP1 (Beta). For those of you who have downloaded the beta, this post won't do you much good, as the ability to set file associations for ClickOnce applications is already built in there. But for the rest of you, I've built a Visual Studio package based on upon a prototype I worked on for the Beta.

About the package: The package exposes an editor to work with file associations in the app.manifest file, as I wrote about in an earlier post. The editor tries to provide some error and warning feedback to cover the cases I mentioned in that post.

Installing the package: A link is provided at the bottom of the post to install the package.

Launching the editor: To bring up the editor, bring up the context menu of a ClickOnce publishable project, and select "Edit File Associations...". The menu item should only be available for ClickOnce applications. The menu item is disabled for other project flavors; I couldn't figure out how to make a menuitem invisible (one would think setting the .Visible property to false would do it, but you would be sadly mistaken).

Launching the editor

 About the Editor: Here's a screen shot of the editor. I'll go into the pieces below.

File Associations Editor

Grid: Sets the 4 values required for file associations. Editing is done in-place, but new items must be added via the New button. Errors and tool tips appear when an invalid value is set in the grid.

New button: Adds a new row to the grid, with some default values.

Delete button: Deletes a row from the grid.

Warning: Appears if some project properties would prevent the installation of the file associations from being valid.

Feedback/Updates: I imagine there are a few problems with the package. If you have issues, please add a comment, and I'll try to update when I can.

Posted by mwade | 1 Comments

Attachment(s): FileAssociationsSetup.msi

How To: Add File Associations to a ClickOnce Application

A little-known feature that made it into .NET Framework 3.5 is the ability for a ClickOnce application to be associated with document extensions. That is, once properly configured, double-clicking on a document can cause your ClickOnce application to launch. Right now, there is no direct tooling support for this feature (and if you ask me the MSDN information doesn't tell the whole story), but in this post I'll show you how you can get file associations added to your ClickOnce application.

Adding File Associations to Your ClickOnce Manifest

File associations are adding to your ClickOnce manifest by specifying the fileAssociation tag and it's 4 attributes:  extension, description, progid, and defaultIcon. This new tag is in the clickonce.v1 namespace. So, one example looks like this:


<fileAssociation xmlns="urn:schemas-microsoft-com:clickonce.v1"
    extension=".mwade"
    description="MWadePad Document"
    progid="MWadePad.Document"
    defaultIcon="mwade.ico"
/>

A little background information about all of these:

  • extension: This is the file extension for which your application will be associated. You should make sure that your extension adds the "." to the front of the file. You should aim to use a unique extension for your file association. If, at install time, ClickOnce determines that this file association is already being used, it won't add the necessary system information to register your file association.
  • description: A brief statement describing the type of document. This appears in the tooltip when hovering over the document, as well as the Type column in explorer when looking at the document.Description Tooltip
  • progid: Helps tie the file extension to your ClickOnce application.
  • defaultIcon: The icon file from the ClickOnce application to use as the document icon

Of course, adding file association information to your application manifest is only half of the battle. How can you tell if your application was launched by launching one of its documents? Well, this information is stored in the ActivationData of the current app domain:

AppDomain.CurrentDomain.SetupInformation.ActivationArguments.ActivationData[0]

Even if the application is activated through file association, the standard ClickOnce update servicing will be applied.

Adding File Associations through Visual Studio

There is no fancy UI to allow you to add file associations to your ClickOnce application in Visual Studio. It is also not possible to add the information to your application manifest after publishing, because that would invalidate the signature. So, does that mean you are forced to modify your manifest and then re-sign with mage after publishing?

Nope. When publishing in Visual Studio, it is possible to add a app.manifest file to your project. This app.manifest acts as an base application manifest, whose information gets added into the generated application manifest. The manifest can be added by clicking the "Enable ClickOnce Security Settings" checkbox on the Security property page.

For example, here is what my app.manifest file looks like when adding the information from above:

<?xml version="1.0" encoding="utf-8"?>
<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <assemblyIdentity version="1.0.0.0" name="MyApplication.app" />
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <applicationRequestMinimum>
        <PermissionSet class="System.Security.PermissionSet" version="1" Unrestricted="true" ID="Custom" SameSite="site" />
        <defaultAssemblyRequest permissionSetReference="Custom" />
      </applicationRequestMinimum>
    </security>
  </trustInfo>

  <fileAssociation xmlns="urn:schemas-microsoft-com:clickonce.v1"
      extension=".mwade"
      description="MWadePad Document"
      progid="MWadePad.Document"
      defaultIcon="mwade.ico"
  />

</asmv1:assembly>

Additional Notes

There are some rules and regulations that need to be pointed out for file associations in ClickOnce:

  • Values must be given for all 4 xml attributes
  • File associations only work for pure ClickOnce applications. That means that browser-hosted applications and Office documents can't take advantage
  • The application must be targeting the 3.5 version of the .NET Framework
  • The application must be full trust
  • The application must be installed (not run from web)
  • There is a limit of 8 file associations per application
Posted by mwade | 15 Comments

Attachment(s): MWadePad.zip

How To: Archive your source files when publishing

A long time ago, when I was trying to look up some piece of information for some blog entry, I stumbled upon a neat site for MSBuild tasks: the MSBuild Community Tasks site. There are several useful build tasks in there, one of which will be the focus of this post: the Zip task. I use this task to package up the source for publication on this blog, and I also use it in the programs I right for entertainment at home.

My archival system for the programs I write in my spare time is pretty simplistic: when I have finished adding a feature and it feels like a good time to use my application, I zip up the sources. Of course, I'll start adding features later, sometimes months or even years later. When I spend so much time away from the program, I'm worried that I'll mess it up so much that I'll just want to revert back to the previous version and start over (that's right: I don't write unit tests at home). Having this zip archive is really handy. Sure, I could use VSS or something fancy, but frankly the programs I write generally don't warrant anything like that. In this entry, I'm going to show how I use the Zip task for archiving.

Since Visual Studio 8 came along, I tend to distribute my applications via ClickOnce. This is a natural time to do the archiving: publishing your app means its a good time to archive the sources. Ah, but when to use the task? Build has both the PreBuild and PostBuild events, which is the old-school way of modifying your build via a command-line command (a .bat file, script, or .exe). The new-school way is to override the BeforeBuild or AfterBuild tasks: I don't know of any good way to do this aside from editing the project file by hand, but I have no qualms about that. Wouldn't it be nice if there was a similar extension point for before or after publishing? Good news: there is.

In Visual Studio, the publish is basically done in 2 phases: there is the build (which in addition to building your binaries, will generate and sign your manifests), and the publish (which pushes all of the appropriate files to the appropriate locations). The first step is all done with MSBuild: it's running msbuild with Target="Publish". The Publish target primarily depends on 2 other targets: Build and PublishOnly:

<PropertyGroup> 
    <PublishDependsOn  Condition="'$(PublishableProject)'=='true'"> 
        SetGenerateManifests; 
        Build; 
        PublishOnly 
    </PublishDependsOn> 
</PropertyGroup>

If you were to take a closer look at what Build depends on, you would see the BeforeBuild and AfterBuild targets, as well as the PreBuildEvent and PostBuildEvent targets. And the PublishOnlyDependsOn property is set to:

<PropertyGroup> 
    <PublishOnlyDependsOn> 
        SetGenerateManifests; 
        PublishBuild; 
        BeforePublish; 
        GenerateManifests; 
        CleanPublishFolder; 
        _CopyFilesToPublishFolder; 
        _DeploymentGenerateBootstrapper; 
        ResolveKeySource; 
        _DeploymentSignClickOnceDeployment; 
        AfterPublish 
    </PublishOnlyDependsOn> 
</PropertyGroup>

Note the BeforePublish and AfterPublish extension points. So it is possible to perform some tasks simply by overriding the AfterPublish target.

Figuring out what to zip is a matter of taste. I make the basic assumption that potentially anything under my project directory is a good zipping candidate. However, there are some directories I want to exclude:

  • obj
  • bin
  • TestResults
  • publish

Similarly, there are several file extensions to avoid, mostly because I only care about the sources:

  • suo: I think this is a file to store user settings for the solution
  • pfx: the signing key for my published application. Definitely don't want to include this just in case I give the zip file to someone else without checking the contents
  • ncb: I think this is intellisense info for VC++ projects
  • user: the user settings for the project file
  • zip: prior archives

So the full information to do the zipping is below:

<Import  Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" /> 
<ItemGroup> 
  <DirectoryToNotZip  Include="obj"> 
    <Visible>False</Visible> 
  </DirectoryToNotZip> 
  <DirectoryToNotZip  Include="bin"> 
    <Visible>False</Visible> 
  </DirectoryToNotZip> 
  <DirectoryToNotZip  Include="TestResults"> 
    <Visible>False</Visible> 
  </DirectoryToNotZip> 
  <DirectoryToNotZip  Include="publish"> 
    <Visible>False</Visible> 
  </DirectoryToNotZip> 
</ItemGroup> 
 
<ItemGroup> 
  <ExtensionToNotZip  Include="suo"> 
    <Visible>False</Visible> 
  </ExtensionToNotZip> 
  <ExtensionToNotZip  Include="pfx"> 
    <Visible>False</Visible> 
  </ExtensionToNotZip> 
  <ExtensionToNotZip  Include="ncb"> 
    <Visible>False</Visible> 
  </ExtensionToNotZip> 
    <ExtensionToNotZip  Include="user"> 
        <Visible>False</Visible> 
    </ExtensionToNotZip> 
    <ExtensionToNotZip  Include="zip"> 
        <Visible>False</Visible> 
    </ExtensionToNotZip> 
</ItemGroup> 
   
<Target  Name="AfterPublish"> 
  <CreateItem  Include="$(ProjectDir)\**\%(DirectoryToNotZip.FileName)\**"> 
    <Output  TaskParameter="Include"  ItemName="FilesToNotZip" /> 
  </CreateItem> 
  <CreateItem  Include="$(ProjectDir)\**\*.%(ExtensionToNotZip.FileName)"> 
    <Output  TaskParameter="Include"  ItemName="FilesToNotZip" /> 
  </CreateItem> 
  <CreateItem  Include="$(ProjectDir)\**\*.*"  Exclude="@(FilesToNotZip)"> 
    <Output  TaskParameter="Include"  ItemName="FilesToZip" /> 
  </CreateItem> 
  <FormatVersion  Version="$(ApplicationVersion)"  Revision="$(ApplicationRevision)"  FormatType="Path"> 
    <Output  TaskParameter="OutputVersion"  PropertyName="PackageVersion" /> 
  </FormatVersion> 
  <Zip  Files="@(FilesToZip)"  ZipFileName="$(ProjectDir)\$(ProjectName)-$(PackageVersion)-src.zip" /> 
</Target>

First, need to import the targets file which contains the Zip task information.

Then, I set the directories and file extensions I don't want to zip in Items. I use items because I build up the list of files to zip by making three calls to CreateItem. The first 2 generate a complete list of files to not zip, and I use that as the list of files to Exclude in the third call. My guess is that I did this in a totally lame way; there's probably some MSBuild stuff I don't know to make the whole process easier, but I'm not entirely sure what it is.

Next, there is a call to the FormatVersion task to figure out what to call my zip file. FormatVersion will take the version of the app that I am publishing (1.0.0.0) and change it into something that may be better for file names (1_0_0_0).

Finally, there is the call to Zip to actually do the archiving. The archive ends up in the project directory.

It would probably be better to zip up my entire solution directory. This would then include the unit tests for my application, and the sources for and class libraries my application is using. I will leave that as an exercise for the reader.

Posted by mwade | 1 Comments

How To: Update the bootstrapper to install a ClickOnce application using Internet Explorer

For completeness sake, I decided to add the postbuild step for using a modified bootstrapper to launch a ClickOnce deployment manifest (.application) file using Internet Explorer. As I detailed here , the original use for this change was to work-around a ClickOnce liimitation when attempting to install the app via Fire Fox. We have since discovered another use for this type of work-around: the bootstrapper may claim it can not find the .application file on a web server if the application file contains DBCS characters. By using the stub executable to launch Internet Explorer, the problem should be avoided.

The task works by re-defining what the PublishOnly target does, replacing _DeploymentGenerateBootstrapper with a new target, CreateBootstrapper. This target is what calls GenerateExtractedBootstrapper. Note that this task is conditioned to run only if the IsWebBootstrapper property is true.

Without more ado, here are the steps necessary to get the modified bootstrapper up and running:

  1. Download and install the binaries for post-build steps (see the attachment of this post)
  2. Unload your project and edit the project file (via the project node's context menu)
  3. Towards the bottom of the file, there should be a comment which starts with To modify your build process. Immediately below the end of that comment, paste the following XML:
    <Import Project="$(MSBuildExtensionsPath)\SetupProjects\SetupProjects.Targets" /> 
    <PropertyGroup> 
        <PublishOnlyDependsOn> 
            SetGenerateManifests; 
            PublishBuild; 
            BeforePublish; 
            GenerateManifests; 
            CleanPublishFolder; 
            _CopyFilesToPublishFolder; 
            CreateBootstrapper; 
            ResolveKeySource; 
            _DeploymentSignClickOnceDeployment; 
            AfterPublish 
        </PublishOnlyDependsOn> 
    </PropertyGroup> 
    
    <Target Name="CreateBootstrapper"> 
        <CallTarget Targets="_DeploymentGenerateBootstrapper" Condition="'$(IsWebBootstrapper)'!='true'" /> 
        <CallTarget Targets="GenerateExtractedBootstrapper"
                    Condition="'$(IsWebBootstrapper)'=='true' and '$(BootstrapperEnabled)'=='true'" /> 
    </Target> 
    
    <Target Name="GenerateExtractedBootstrapper"> 
        <GenerateBootstrapper ApplicationFile="stub.exe"
                              ApplicationName="$(AssemblyName)"
                              ApplicationUrl=""
                              BootstrapperItems="@(BootstrapperPackage)"
                              ComponentsLocation="$(BootstrapperComponentsLocation)"
                              ComponentsUrl=""
                              Culture="$(TargetCulture)"
                              FallbackCulture="$(FallbackCulture)"
                              OutputPath="$(PublishDir)"
                              SupportUrl="$(_DeploymentFormattedSupportUrl)"
                              Path="$(GenerateBootstrapperSdkPath)" /> 
        <GenerateExtractedBootstrapper ApplicationFile="$(TargetDeployManifestFileName)"
                                       ApplicationUrl="$(_DeploymentFormattedApplicationUrl)"
                                       BootstrapperFile="$(PublishDir)\setup.exe"
                                       ComponentsUrl="$(_DeploymentFormattedComponentsUrl)"
                                       OutputPath="$(PublishDir)"
                                       StubExe="$(MSBuildExtensionsPath)\SetupProjects\InstallApplication.exe" /> 
    </Target>
    

    Notice the steps are a little shorter than that for the setup project. That's because the necessary projects are already msbuild-based. One draw-back to this solution is that IExplorer will flash onto the screen when the bootstrapper is finished, at least until the file handler within Internet Explorer is able to take over. I do wonder if this could be avoided with smarter parameters to Internet Explorer (for example, how could you get the function call to re-use an existing Internet Explorer session).

How To: Update the bootstrapper to accept command-line arguments

In the previous blog entry, I went over the design and implementation of a modified bootstrapper which accepts command-line parameters. For example the bootstrapper which shipped with Visual Studio 2005 is not capable of "doing the right thing" for the following command:

setup.exe -qn

With the modifications, it is possile to get the MSI to install silently. Note the qualifier there: if the bootstrapper is installing prerequisites, it is not possible to get those to siliently install: during the design phase, we were told by the crack team of Microsoft lawyers that some UI (especially the EULA) had to be shown to end-users.

There are a few other limitations to the modified bootstrapper. Off-hand, I can think of the following:

  • If the MSI is signed, the signature is NOT verified by the bootstrapper
  • The error handling within the new bootstrapper (including an MSI which fails to initialize properly) is not well-handled
  • The testing of the modified bootstrapper is not up to Microsoft's standards

Without more ado, here are the steps necessary to get the modified bootstrapper up and running:

  1. Download and install the binaries for post-build steps (see the attachment of this post)
  2. Make sure that msbuild is on your path. You can do this by launching devenv from the Visual Studio command prompt
  3. Create your setup project
  4. Copy the template below into a file called "PostBuild.proj" in the same directory as your .vdproj file
  5. Add PostBuild.proj to your setup project
  6. Set Exclude to "True" for PostBuild.proj
  7. Set the PostBuildEvent (in the property grid) to:
    msbuild.exe /p:Configuration="$(Configuration)" /p:BuiltOutputPath="$(BuiltOuputPath)" /p:ProjectDir="$(ProjectDir)\" /p:BuiltOutputDir="$(ProjectDir)$(Configuration)" "$(ProjectDir)\PostBuild.proj"
  8. Turn off prerequisite generation for your setup project
  9. Update the properties in PostBuild.proj to match your particular installer (see images below to see how the MSBuild properties correspond to setup project properties). Do not ignore this step. At the very least, you need to update the ApplicationFile value in the PostBuild.proj file. (I just spent 15 minutes debugging trying to figure out why my extracted bootstrapper stopped working. It turns out it was because of this)
  10. Build your setup project

Setup project property grid

Setup property page

Bootstrapper dialog

Microsoft Data Access Components 2.8 Microsoft.Data.Access.Components.2.8
.NET Framework 2.0 Microsoft.Net.Framework.2.0
Crystal Reports for .NET Framework 2.0 BusinessObjects.CrystalReports.NET.2.0
Microsoft Visual J# .NET Redistributable Package 2.0 Microsoft.JSharp.2.0
Microsoft Visual Studio 2005 Report Viewer Microsoft.ReportViewer.8.0
Visual C++ Runtime Libraries (x86) Microsoft.Visual.C++.8.0.x86
Windows Installer 2.0 Microsoft.Windows.Installer.2.0
Windows Installer 3.1 Microsoft.Windows.Installer.3.1
SQL Server 2005 Express Edition Microsoft.Sql.Server.Express.1.0
Bootstrapper packages and corresponding Product Codes
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="PostBuild"> 
    <Import Project="$(MSBuildExtensionsPath)\\SetupProjects\\SetupProjects.Targets" /> 
 
    <PropertyGroup> 
        <PostBuildDependsOn></PostBuildDependsOn> 
    </PropertyGroup> 
     
    <PropertyGroup> 
        <ApplicationFile>SetupExtractedBootstrapper.msi</ApplicationFile> 
        <ApplicationName>SetupExtractedBootstrapper</ApplicationName> 
        <ApplicationUrl></ApplicationUrl> 
        <ComponentsLocation>HomeSite</ComponentsLocation> 
        <ComponentsUrl></ComponentsUrl> 
        <Culture></Culture> 
        <OutputPath>$(BuiltOutputDir)</OutputPath> 
    </PropertyGroup> 
 
    <ItemGroup> 
        <BootstrapperPackage Include="Microsoft.Net.Framework.2.0" /> 
    </ItemGroup> 
 
    <Target Name="PostBuild" DependsOnTargets="$(PostBuildDependsOn)"> 
        <GenerateBootstrapper ApplicationFile="stub.exe"  
                              ApplicationName="$(ApplicationName)" 
                              ApplicationUrl="" 
                              BootstrapperItems="@(BootstrapperPackage)"  
                              ComponentsLocation="$(ComponentsLocation)" 
                              ComponentsUrl="" 
                              Culture="$(Culture)" 
                              OutputPath="$(OutputPath)" /> 
         
        <GenerateExtractedBootstrapper ApplicationFile="$(ApplicationFile)"  
                                       ApplicationUrl="$(ApplicationUrl)"  
                                       ComponentsUrl="$(ComponentsUrl)"  
                                       OutputPath="$(OutputPath)" 
                                       BootstrapperFile="$(OutputPath)\\setup.exe" 
                                       StubExe="$(MSBuildExtensionsPath)\\SetupProjects\\InstallMsi.exe" /> 
    </Target> 
</Project>

Kicking back with Motley Crue, sippin' my LowenBrau

Introduction

A common complaint about the bootstrapper that shipped with Visual Studio 8 is that it does not accept command-line parameters for launching the MSI. For example, lots of people want to turn on verbose logging or quiet install. The reason for this limitation is because of how the MSI is installed: instead of using msiexec.exe /i to install the package, a call to MsiInstallProduct is used. MsiInstallProduct only accepts command-line parameters which affect the properties used in the install (which the bootstrapper does dutifully pass along). However, if we are willing to circumvent the use of MsiInstallProduct to install the MSI, it is possible to get the command-line options you desire. I'm going to show how it can be done in this entry. The design will be similar to the design I came up with in December 2005 for a different problem.

It was around that time that the ClickOnce design-time team was made aware of a slight problem that the ClickOnce runtime already knew about: launching a ClickOnce application through a browser other than IE simply didn't work (and how this was not a violation of the anti-trust settlement still baffles me to this day). There were blogposts pointing out this little fact. The ClickOnce team's official recommendation to all FireFox users was to use Internet Explorer to launch a ClickOnce application (reading that statement makes me roll my eyes, even 2 years later). But there was one little problem with this work-around they were suggesting: the bootstrapper.

Ahh, the bootstrapper. When the bootstrapper is coming from a website (http://www.foo.com), the bootstrapper's caboose install was nothing more than a ShellExecute to the ClickOnce application (http://www.foo.com/bar.application). The bootstrapper was relying on the shell handler within the OS to route this call to the proper application. It never occurred to the tooling team (us) that FireFox might not be able to correctly handle a .application file. Of course the runtime team knew it wouldn't work: there was no sanctioned way for the FireFox to correctly deal with a .application file. So the runtime team pressured the designtime team to come up with a solution to what was essentially their problem (and if it sounds like I'm still bitter about this, well...). The runtime team's complaint was that their recommended work-around of using IE didn't work when someone used IE to navigate to setup.exe but FireFox was the default browser on the box. After 2 weeks of arguing (which I still can't believe we lost. But I'm not bitter!), our team finally capitulated and agreed to try to come up with a patch for this problem. I wrote up a design document.

Fortunately, the FireFox community doesn't take 2 weeks to solve a problem. By the time the document was written, someone wrote a FireFox plugin that was able to handle a ClickOnce application. So, the plan was never acted upon: with the FireFox plugin there was no need to do it.

But despite the rage I felt (and still feel today, although I'm getting over it. Really), I found the experience to be useful. It was a result of this that I first realized MSBuild was pretty cool. Even though I had written the GenerateBootstrapper task within MSBuild, I didn't really realize what was going on. I wrote the task to serve two different systems: the setup projects (which used COM) and the ClickOnce publishing. I hooked up setup projects to have setup projects use the code in Microsoft.Build.Tasks.dll, while someone else hooked up the task to be used from the ClickOnce publishing system. Although I saw how the GenerateBootstrapper task was part of the publishing system, I still didn't really understand how MSBuild worked. But planning this change helped me realize 2 of the most powerful upsides to MSBuild:

  1. A user could write their own custom build steps
  2. Those build steps could be inserted anywhere into the build process

The solution I came up with for the ClickOnce/FireFox problem took advantage of these facts. The simplest fix for the problem would be to change the bootstrapper to always launch Internet Explorer when attempting to launch a ClickOnce application. But in order to do that, we would have had to re-ship the setup.bin that shipped with the .Net SDK and Visual Studio 8. I didn't like that plan given how difficult and expensive it is for Microsoft to produce and support a GDR and how stupid Microsoft would look for producing this so soon after shipping.

My proposed solution was to change what the bootstrapper launched at the end of the installation: instead of calling ShellExecute on the web-address to the ClickOnce deployment manifest, the bootstrapper would call ShellExecute on a stub exe, which would call Internet Explorer to launch the ClickOnce deployment manifest. And to avoid multiple download security prompts, both of these executables would be stuffed as resources inside of what was termed an "uber-bootstrapper" which would extract both of these files to the installing users temporary directory and run the bootstrapper from there. The uber-bootstrapper leverages the existing bootstrapper and all of the prerequisite checking and running that it does.

To accomplish this, I proposed a new MSBuild task called GenerateExtractedBootstrapper. This is the task that would take a "regular" bootstrapper along with the stub exe and stuff them into its resources. Then, to have the publish process start calling this task, all a user had to do was modify the PublishOnlyDependsOn property to include the GenerateExtractedBootstrapper.

I coded this up and it seemed to work. Fortunately, the plans were shelved because someone built the FireFox plugin to handle ClickOnce manifests.

I always thought there was some merit to this solution, though, and could be applied to other areas as well. A solution like the ClickOnce/FireFox solution could alleviate this command-line issue: instead of a stub exe which launched Internet Explorer, why not have the stub exe launch msiexec? We never pursued this possibility because there wasn't enough general outcry for it.

Before I start the process of creating this extracted bootstrapper, I did a little re-arranging of the solution that has been developed thus far. These changes include:

  • I created a new namespace, SetupProjects.WindowsInstaller, which now includes everything that has to do with WindowsInstaller: some of the Utility functions and the WindowsInstaller-specific tasks
  • Moved windows installer tasks (those that inherited from SetupProjectTask) into SetupProjects.WindowsInstaller.Tasks
  • Renamed SetupProjectTask to WindowsInstallerTask
  • Re-worked the tests to fix compiler errors and make sure they contine to pass (they do)
  • Moved all setup projects and other sample projects into a solution directory, "Examples"

Step 1: The original bootstrapper

Before we go about creating a fancified bootstrapper, let's first discuss how to go about creating the original bootstrapper. Let's create a new project for our bootstrapper experiments, called SetupExtractedBootstrapper. Add a baseline project file to the project which will contain all of our postbuild steps:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"  DefaultTargets="PostBuild"> 
    <Import Project="$(MSBuildExtensionsPath)\SetupProjects\SetupProjects.Targets" /> 
 
    <PropertyGroup> 
        <PostBuildDependsOn></PostBuildDependsOn> 
    </PropertyGroup> 
 
    <Target Name="PostBuild" DependsOnTargets="$(PostBuildDependsOn)"> 
    </Target> 
</Project>

And set the postbuild event to use MSBuild build the project file:

msbuild.exe /p:Configuration="$(Configuration)" /p:BuiltOutputPath="$(BuiltOuputPath)" /p:ProjectDir="$(ProjectDir)\" /p:BuiltOutputDir="$(ProjectDir)$(Configuration)" $(ProjectDir)\PostBuild.proj"

In order to work, you must make sure that MSBuild is on your path. Launching devenv from the Visual Studio 2005 Command Prompt should be sufficient.

To generate the bootstrapper, you need to use the GenerateBootstrapper task. There are many inputs to this task, but let's quickly enumerate the relevant ones:
  • ApplicationFile: The name of the caboose (the Msi to launch once all prerequisites have been installed
  • ApplicationName: The name of the application being installed. This value is used with the install UI
  • ApplicationUrl: The expected web address of the bootstrapper and msi it will be installing
  • BootstrapperItems: The list of prerequisites the bootstrapper will install
  • ComponentsLocation: Where the bootstrapper will get the prerequisites it will be installing. Can be one of three values (if blank, HomeSite is assumed):
    • HomeSite: Allow the bootstrapper package to be downloaded from the bootstrapper package vendor's website
    • Relative: The bootstrapper packages are found relative to the location of setup.exe
    • Absolute: Bootstrapper package are downloaded from a specific website
  • ComponentsUrl: The website to use if ComponentsLocation is Absolute
  • Culture: Specifies which language the bootstrapper UI should be shown in, and the language of the bootstrapper package to install. Can generally be left blank
  • OutputPath: The path on disk to which the bootstrapper will be built

All of these values correspond to something in the setup project UI, and several are part of the property set passed into the project file. Unfortunately, it won't be possible to avoid some of the duplication. See the pictures below for a summary of how setup project properties map to GenerateBootstrapper task parameters.

Setup project property grid

Setup project property page

 Prerequisites dialog

For this particular example, the following properties should be set to generate the same default bootstrapping experience:

<PropertyGroup> 
    <ApplicationFile>SetupExtractedBootstrapper.msi</ApplicationFile> 
    <ApplicationName>SetupExtractedBootstrapper</ApplicationName> 
    <ApplicationUrl></ApplicationUrl> 
    <ComponentsLocation>HomeSite</ComponentsLocation> 
    <ComponentsUrl></ComponentsUrl> 
    <Culture></Culture> 
    <OutputPath>$(BuiltOutputDir)</OutputPath> 
</PropertyGroup> 
 
<ItemGroup> 
    <BootstrapperPackage Include="Microsoft.Net.Framework.2.0" /> 
</ItemGroup>

<Target Name="PostBuild" DependsOnTargets="$(PostBuildDependsOn)"> 
    <GenerateBootstrapper ApplicationFile="$(ApplicationFile)" 
                          ApplicationName="$(ApplicationName)" 
                          ApplicationUrl="$(ApplicationUrl)" 
                          BootstrapperItems="@(BootstrapperPackage)"
                          ComponentsLocation="$(ComponentsLocation)" 
                          ComponentsUrl="$(ComponentsUrl)" 
                          Culture="$(Culture)" 
                          OutputPath="$(OutputPath)" /> 
</Target>

The most notable thing in here is the BootstrapperPackage item. The Include property is set to "Microsoft.Net.Framework.2.0". Where did that value come from? It corresponds to the ProductCode of the .Net Framework 2.0 bootstrapper package. These values are discoverable if you know where to look. You can find these by going to the product.xml file which represents the bootstrapper package. Perhaps the easiest way, though, is to open the Prerequisites dialog for your project, check off the packages you want, save the project file, and view it in notepad.

Make sure the bootstrapper is turned off in the Prerequisites page. Building the project should succeed, and you should see that a setup.exe was created in your build output directory. Running it seems to work as we would expect.

Step 2: The uber-bootstrapper

In designing the uber-bootstrapper, I'm going to make one important simplifying assumption: the new bootstrapper will not need to work on Windows 98 systems.

The design of the bootstrapper is pretty simple, and borrows quite a bit from the original bootstrapper. Properties wil be set in the resources of the uber-bootstrapper, and can be extracted by a call to FindResource. Files will also be stuffed into the resources of the uber-bootstrapper. There will be 2 different files: the original bootstrapper, and a stub executable that performs the install. When the uber-bootstrapper is invoked, it will extract its 2 files into a temporary directory. Both of these extracted files needs to know something about its origins in order to do their job. The original bootstrapper needs to know where to get the prerequistes it will be installing as well as the caboose application to invoke. The latter is easy (it's the stub exe that was extracted to the temporary directory). The first is a little more challenging.

One of the options for the ComponentsLocation is "Relative". In that case, the relative path should be the source location of the uber-bootstrapper, whether it was from a web-site, CD, or disk path. The bootstrapper is designed to look the caboose in the exact same spot. Because we want the caboose extracted to the temporary directory to be launched, we want the Relative location to be blanked out, signalling the original bootstrapper to look for the caboose next to itself. The uber-bootstrapper will set the the ComponentsUrl to the uber-bootstrapper location of origin to look for application prerequisites. The stub executable will have similar resource information added to it after extraction so that it knows where to download the MSI from.

Step 3: The stub executable

The stub executable itself is pretty simple. It first figures out where the heck msiexec is by using the InstallerLocation registry key described on MSDN. It next finds the location to the msi it will be launching by looking at its resources. Finally, it constrcuts the full spectrum of command-line parameteres (msiexec -i "Path To Msi" ) and runs ShellExecute. It should be noted that one feature the standard bootstrapper does that the stub I developed does not is make calls to WinVerifyTrust to make sure that consistent trust decisions are made regarding a signed bootstrpper package and a signed MSI. Note that the calls are only made when installing from a website. It should be possible to add this information in, I just chose not to to keep the code simpler.

Step 4: The final build task

Having laid the ground work for the binaries that are necessary for this scheme to work, let's get them built. The properties set in the project file should remain exactly the same. However, their usage by the GenerateBootstrapper task are slightly different:

<Target Name="PostBuild" DependsOnTargets="$(PostBuildDependsOn)"> 
    <GenerateBootstrapper ApplicationFile="stub.exe"
                          ApplicationName="$(ApplicationName)" 
                          ApplicationUrl="" 
                          BootstrapperItems="@(BootstrapperPackage)"  
                          ComponentsLocation="$(ComponentsLocation)"
                          ComponentsUrl="" 
                          Culture="$(Culture)" 
                          OutputPath="$(OutputPath)" />

In particular, the ApplicationFile, ApplicationUrl, and ComponentsUrl are all required to change. Don't be alarmed, however: these values simply moved into the GenerateExtractedBootstrapper task:

    <GenerateExtractedBootstrapper ApplicationFile="$(ApplicationFile)"  
                                   ApplicationUrl="$(ApplicationUrl)"  
                                   ComponentsUrl="$(ComponentsUrl)"  
                                   OutputPath="$(OutputPath)" 
                                   BootstrapperFile="$(OutputPath)\\setup.exe" 
                                   StubExe="$(MSBuildExtensionsPath)\\SetupProjects\\InstallMsi.exe" /> 
</Target>

Conclusion

So there you have it, a way to modify the behavior of your bootstrapper. Sources are included at the tail end of this post in case you want to add further functionality to the installer. There is also a very small set of helper functions for you to create your own stub exectuable. For completeness sake, I included the source for the original motivation for this endeavor: launching a .application file through Internet Explorer. I only tested these on my laptop, which has VS installed. So I'm a little concerned that I screwed up the properties of the VC projects : in particular, I wanted to statically link in the necessary runtimes, and (I think) build without a manifest. If someone finds I did these wrong, I would love to hear it and correct the mistake.

Updates

11/29/2007: There was a bug in the InstallApplication bootstrapper stub (I mixed around a couple of variables). Attached new sources.

Moving into Del Boca Vista with the Costanza's (Completed)

The last step in the Vista-related improvements I outlined several months ago involves improving the bootstrapper experience on Vista. The problem is that running the bootstrapper on Vista gives a UAC elevation prompt as soon as it is launched. Despite what you may have heard, the bootstrapper isn't some terribly sophisticated piece of software engineering. Really, all it does is launch processes. When the bootstrapper is elevated, the children processes it launches are also elevated. This is both a blessing and a curse. On the one hand, if the bootstrapper is running other installers, those probably should be elevated. On the other hand, it's not always necessary to elevate the final application install (which we refer to as the "caboose"), for example when installing a ClickOnce application or installing an MSI which has the LUA Aware bit set.

One tempting solution to this problem is to not have the bootstrapper prompt for elevation up front. Its the OS that is causing this prompt to come up, but there is a way to tell the Vista to not prompt for elevation: you can add a manifest to setup.exe so that no eleavtion occurs. And that is correct, this will help the situation of launching the caboose elevated. But it breaks any bootstrapper that needs to install prerequisites, because those packages will need to be elevated.

"But Mike," you argue, "won't the auto-elevation features of Vista get this solution to work? After all, when I try to run my SQL Express installer manually an elevation prompt comes up." Unfortunately, its not that simple: Vista will issue the prompt for the prerequisites only when the installer processes are launched in a specific manner. Namely, the process has to be launched with ShellExecuteEx, and specify the verb as "runas". Of course this wasn't known at the time the bootstrapper was getting developed, and as a result that's not how the bootstrapper works. So simply adding a manifest to your setup.exe won't work.

A more exotic solution is to have the bootstrapper change how the caboose is launched. This is possible to accomplish because of how the bootstrapper is architected: if the caboose is an MSI, it uses MsiInstallProduct to install the MSI. Otherwise, it does a ShellExecute on the caboose. So, if the caboose were actually an executable that would launch the MSI for us, that might work instead. Well, almost: when the bootstrapper is elevated by Vista, the caboose will also be elevated.

So, the solution needs to get even more exotic: create one executable that is not elevated, which launches the original setup.exe (specifying the "runas" verb), waits for that to finish, and then installs the MSI. And on the surface, this doesn't seem too hard: once again, all that's basically happening is that process are launched, and something waits until each process is finished before launching the next process. In this case there would be 2 child processes: the original bootstrapper (which may launch additional child processes), and then MSI install. And this would be pretty easy except that some of the child processes launched by the original bootstrapper may cause the machine to reboot. Suddenly this becomes a lot harder: now, this new bootstrapper has to detect when the original bootstrapper is causing a machine reboot, and needs to deal with restarting the original bootstrapper when coming out of the reboot.

I am fully convinced that I could still create a bootstrapper that deals with all of these scenarios. But I won't. The fact of the matter is, the user experience would still be far from ideal, and getting this "uber-bootstrapper" technique fully tested enough to publish to the web just won't happen in a timely fashion. If this were 2006 instead of 2007 and Visual Studio 9 weren't right around the corner, I would make the time. But the fact of the matter is that VS 9 is going to fix this problem for us (with a better solution than I could come up with on this blog), so there's not much sense in writing a blog post thats going to be completely obsolete in a matter of months. And yes, the Visual Studio 9 solution will be free (as part of the Windows SDK) and can be used on Visual Studio 8 projects (I plan on blogging about that once the SDK is officially released).

So, that will wrap up the Vista-specific post-build steps I wanted to blog about. I'll summarize them below, along with a few words about how Visual Studio 9 (aka Orcas) will address those issues.

Making Custom Actions run without impersonation
Orcas will be making all custom actions NoImpersonate.

Create non-advertised shortcuts
Orcas will continue to have all shortcuts be Advertised, so you might want to hold onto these steps.

LUAAware bit
Orcas won't provide any means to access the LUAAware bit.

Shield Icons
No Vista-related changes to standard Windows Installer dialogs are coming in Orcas.

Bootstrapper Improvements
Improvments were made in this area. A manifest was added to setup.exe to not force elevation on Vista. The corresponding changes to how the prerequisites (with the "runas" verb) were also made. As a result, there is no elevation prompt when the bootstrapper is launched, and therefore the caboose install will not automatically be elevated. However, there will be an elevation prompt for every prerequisite that is installed on the machine. So, if your application needs the 3.5 .NET Framework as well as SQL Express, installing your applicatoin via the bootstrapper will cause 2 elevation prompts (1 for each of the components). But this is much better than the VS 8 behavior, which caused an elevation prompt even if nothing was installed.

Posted by mwade | 1 Comments

Lobster thermidor aux crevettes with a Mornay sauce garnished with truffle paté, brandy and with a fried egg on top and ...

A little while after I published my fake mailbag question, I decided to change my blog settings so that email and comments posted to the blog would be forwarded to my work email address. The blog automatically posts some comments, but it turns out a great deal of them are filtered automatically. How many? I had no idea, until they started getting sent (but not posted).

I noticed a distinctive trend amongst the spam comments that were passed along (and no, there haven't been any "real" comments or emails passed sent, but I'm not surprised): they all tended to be one word and feature car brands or map-quest in the links. I also noted there were only 4 different words used, with varying amounts of punctuation.

So out of morbid curiosity, I decided to track all of the comments. Here are the results, in case you are also morbidly curious:

Comment Frequency
Cool. 47
Cool... 49
Cool! 40
Interesting... 44
interesting 51
Nice 36
Nice... 37
Nice 37
Sorry :( 44

385 comments thus far.

My favorite? I sort of like the "Sorry :(". For some reason, its good to know the spammers empathize with me, even if the empathy they are feeling is for a technical blog. It shows the spammers are human, too.

Posted by mwade | 1 Comments

Select-Play-Select-3-0-Select

Last time, I created an installer with a shortcut in the user's start menu to a very simple application. Once that application is installed, it should be possible to right-click the shortcut and select "Run As Administrator" if you wanted to run the application as an administrator. Unfortunately, that option is not available for the shortcut installed with my Windows Installer package. What gives?

Context menu for advertised shortcut

It turns out that advertised shortcuts do not have this option, while non-advertised shortcuts do. How can someone tell which shortcuts are advertised and which are non-advertised? Well, if you're using a setup project, they are all advertised. To make them non-advertised, we'll need a little post-build magic.

The Shortcut Table entry in MSDN describes what it means to have a shortcut be non-advertised:

The field (Target) should contains a property identifier enclosed by square brackets ([ ]), that is expanded into the file or a folder pointed to by the shortcut.

That should clear things up. If not, look at the example in the Specifying Shortcuts walkthrough: the non-advertised shortcuts are formatted strings of the form [#filekey]. So the good news is it won't be necessary to write any new tasks to accomplish this. The bad news is, we'll have to do some funky stuff to get MSBuild to do what we need it to do.

Start by creating the setup project for installing: SetupNonAdvertisedShortcuts. I had this project match a lot of the behavior of SetupNoElevation from last time. I also added another executable, CommandLineArgs.exe from SetupLaunchapplication because I wanted to make this example a little more tricky. I added a shortcut to the SimpleApplication to both the Desktop and the start menu, and added a shortcut to CommandLineArgs.exe to the start menu.

Next, add a postbuild step. Let's just add the project file from last time around, with the usual command-line parameters. This project file will do 2 different tasks: the SummaryInfo update from last time, as well as the non-advertised shortcuts for this time. So, the project file will start off like this:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="PostBuild"> 
    <Import Project="$(MSBuildExtensionsPath)\\SetupProjects\\SetupProjects.Targets" /> 
 
    <PropertyGroup> 
        <PostBuildDependsOn>NoElevation;NonAdvertisedShortcuts</PostBuildDependsOn> 
    </PropertyGroup> 
 
    <Target Name="PostBuild" DependsOnTargets="$(PostBuildDependsOn)" /> 
 
    <!--  
            No Elevation target 
    --> 
     
    <PropertyGroup> 
        <PID_WORDCOUNT>15</PID_WORDCOUNT> 
    </PropertyGroup> 
 
    <Target Name="NoElevation"> 
        <GetSummaryInfo MsiFileName="$(BuiltOutputPath)" 
                        PropertyIdentifiers="$(PID_WORDCOUNT)"> 
            <Output TaskParameter="SummaryInfo" ItemName="WordCount" /> 
        </GetSummaryInfo> 
 
        <AssignValue Items="@(WordCount)"  
                     Metadata="Value"  
                     Operator="Or"  
                     Value="0x8"> 
            <Output TaskParameter="ModifiedItems" ItemName="UpdatedWordCount" /> 
        </AssignValue> 
 
        <SetSummaryInfo MsiFileName="$(BuiltOutputPath)" 
                        SummaryInfo="@(UpdatedWordCount)" /> 
    </Target> 
 
</Project>

The PostBuild target is empty, but has 2 dependencies: NoElevation and NonAdvertisedShortcuts. NoElevation is exactly the same stuff as last time. So now its only necessary to define the NonAdvertisedShortcuts target.

This Target will convert all shortcut to non-advertised shortcuts. It starts with the assumption that all of the shortcuts within the MSI are pointing to files (as opposed to folders). Ideally, this target would grab all of the entries out of the shortcut table and join these with appropriate entries from the file table, based on Component_ column within each table. The Target column would end up being [#{File.File}]. For example, if we grabbed the following data out of the Shortcut table:

Shortcuts
Shortcut
Component_
_07FF8550467741DF83F0A745A8F8D3B4 C__4FE2FE1E91B9E7CBEA0CC779BA1485CC
_0AC4DC265C1E4F39A9AFCD2B592488AE C__10812A57967D596B3D0597E4309F0C8A
_0C140C82CCD14896AFC19C6B4F386499 C__4FE2FE1E91B9E7CBEA0CC779BA1485CC

and the following out of the File table:

Files
File
Component_
_10812A57967D596B3D0597E4309F0C8A C__10812A57967D596B3D0597E4309F0C8A
_4FE2FE1E91B9E7CBEA0CC779BA1485CC C__4FE2FE1E91B9E7CBEA0CC779BA1485CC

we would end up with a Shortcut data that looked like this:

ModifiedShortcuts
Shortcut
Component_
Target
_07FF8550467741DF83F0A745A8F8D3B4 C__4FE2FE1E91B9E7CBEA0CC779BA1485CC [#_4FE2FE1E91B9E7CBEA0CC779BA1485CC]
_0AC4DC265C1E4F39A9AFCD2B592488AE C__10812A57967D596B3D0597E4309F0C8A [#_10812A57967D596B3D0597E4309F0C8A]
_0C140C82CCD14896AFC19C6B4F386499 C__4FE2FE1E91B9E7CBEA0CC779BA1485CC [#_4FE2FE1E91B9E7CBEA0CC779BA1485CC]

I asked around and an operation like join isn't available in MSBuild: adding the correct File metadata based to the shortcut entry based on the Component_ is tricky. The filtering is possible, but confusing (see this sample question and explanation). However the merging is trickier. A suggestion was made to use the MSBuild task to run a target that does the merging for us, like this:

<ItemGroup> 
    <_ShortcutColumns Include="Component_" /> 
</ItemGroup> 
 
<ItemGroup> 
    <_FileColumns Include="File" /> 
    <_FileColumns Include="Component_" /> 
</ItemGroup> 
 
<Target Name="NonAdvertisedShortcuts"> 
    <Select MsiFileName="$(BuiltOutputPath)" 
            TableName="File" 
            Columns="@(_FileColumns)"> 
        <Output TaskParameter="Records" ItemName="Files" /> 
    </Select> 
 
    <MSBuild Projects="$(MSBuildProjectFile)" Targets="SetShortcutTarget" Properties="Component_=%(Files.Component_);_Target=[#%(Files.File)]"> 
        <Output TaskParameter="TargetOutputs" ItemName="ModifiedShortcuts" /> 
    </MSBuild> 
 
    <Update MsiFileName="$(BuiltOutputPath)" 
            Records="@(ModifiedShortcuts)" />                 
</Target> 
 
<Target Name="SetShortcutTarget" 
        Outputs="@(ModifiedShortcuts)"> 
 
    <GetPrimaryKeys MsiFileName="$(BuiltOutputPath)"  
                    TableName="Shortcut"> 
        <Output TaskParameter="PrimaryKeys" ItemName="_ShortcutColumns" /> 
    </GetPrimaryKeys> 
 
    <Select MsiFileName="$(BuiltOutputPath)" 
            TableName="Shortcut" 
            Columns="@(_ShortcutColumns)" 
            Where="`Component_`='$(Component_)'"> 
        <Output TaskParameter="Records"  ItemName="Shortcuts" /> 
    </Select> 
 
    <CreateItem Include="@(Shortcuts)" AdditionalMetaData="Target=$(_Target)"> 
        <Output TaskParameter="Include" ItemName="ModifiedShortcuts" /> 
    </CreateItem> 
</Target>

In this case, the relevant File information (the Component_ and File key) value is passed through to the SetShortcutTarget target. The SetShortcutTarget gets the primary keys of the Shortcut table, appending the values onto the list of other columns to get (Component_). A call to Select is made, searching for the specific component that was passed as input to the target. The items found then have the Target property set to the input _Target property. Why the _ in front of _Target? MSBuild complained about setting a property called Target. So I made it different by throwing the _ in there. Finally, these Shortcuts are passed back out of the Target via the Outputs value. As the SetShortcutTarget target continues to be called because of the batching, the ModifiedShortcuts list keeps getting bigger and bigger, until all of the File entries have been used, and the msi can now be updated with the Update task.

Is this technique optimal? Not even close. There are 2 hits into the Windows Installer package with each call to SetShortcutTarget: one to get the primary keys of the Shortcut table, and another to select all shortcuts belonging to a specific component. The call to GetPrimaryKeys could be eliminated simply by including the primary key of the Shortcut table (which is Shortcut) in the ItemGroup list. But based on the MSBuild experts I spoke with, there is no avoiding the multiple calls to Select.

Is this technique functional? Yes, it gets the job done. Try installing the generated Windows Installer package to a Vista machine. When you're finished, you'll see the Run as Administrator option in your context menu.

Context menu of non-advertised shortcut

More Posts Next page »
 
Page view tracker