Customize the Default Web Page for a ClickOnce Application

In an article published recently on DevTips.NET (beware: Dutch) I discuss the benefits of ClickOnce deployment. Though a very useful feature in the .NET 2.0 Framework, it has one (ok, perhaps more) simple limitation. The webpage from which one can download and install the ClickOnce deployment installation seems to be hard coded. And that’s no good, since you may want to have additional instructions, your “company style“, and comments in your own language (a reader of my article asked this specific question), etc.

 I’ve tried to find a way to customize this page from the available options within Visual Studio, only to find this note in the online MSDN help:  

To customize the publish Web page

  1. Publish your ClickOnce application to a Web location. For more information, see How to: Publish a ClickOnce Application.

  2. On the Web server, open the Publish.htm file in Visual Web Designer or another HTML editor.

  3. Customize the page as desired and save it.

How foolish is that! You would have to do this every time you publish a new version. There has to be a better way. For lack of finding one on the web, I created one myself.

The default publish.htm file is almost XML. Almost, since it contains the undefined entity   and XML hates that. But with this element out of the way (doing a simple .Replace() ) you can load the file in an XML document. You see where this is going? Right, XSLT transformation. So first, we figure out how to navigate to the various elements. No worry, I did that for you and placed them in tiny templates:

<xsl:template name="BannerTextApplication">
    <xsl:value-of select="//SPAN[@CLASS='BannerTextApplication']"/>
  xsl:template>
  <xsl:template name="BannerTextCompany">
    <xsl:value-of select="//SPAN[@CLASS='BannerTextCompany']"/>
  xsl:template>
  <xsl:template name="ApplicationName">
    <xsl:value-of select="//TABLE/TR[TD/B='Name:']/TD[position()=3]"/>
  xsl:template>
  <xsl:template name="ApplicationVersion">
    <xsl:value-of select="//TABLE/TR[TD/B='Version:']/TD[position()=3]"/>
  xsl:template>
  <xsl:template name="ApplicationPublisher">
    <xsl:value-of select="//TABLE/TR[TD/B='Publisher:']/TD[position()=3]"/>
  xsl:template>
  <xsl:template name="Prerequisites">
    <xsl:copy-of select="//TABLE[position()=2]//UL">xsl:copy-of>
  xsl:template>

Now you can setup the rest of the XSLT according to your own wishes. I suggest you also include the script element from the original Publish.htm file, because it contains bootstrapping code and verifies wheter the client has the .NET 2.0 Framework already installed. For your convenience, a sample XSLT can be downloaded here. As I’m not much of a designer, please take this sample as a starting point.

Now for the actual transformation, the .NET 2.0 Framework has made XslTransform obsolete. We need to use the new XslCompiledTransform class. Here’s the code:

// load the original publish.htm file
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create("...url to the original publish.htm file...");
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
Stream dataStream = response.GetResponseStream();        
StreamReader reader = new StreamReader(dataStream);        
string responseFromServer = reader.ReadToEnd();
// remove the   entity from the html string
responseFromServer= responseFromServer.Replace(" ", " ");
// load the string in an XML document
System.Xml.XmlDocument xml = new System.Xml.XmlDocument();                
xml.LoadXml(responseFromServer);
// setup and perform the XSLT transformation
XslCompiledTransform xslt = new XslCompiledTransform();        
TextWriter html = new StringWriter();
xslt.Load(Server.MapPath("CustomPublish.xsl"));        
xslt.Transform(xml, null, html);
// write the result to the Response object
Response.Write(html.ToString());
Response.End();

That’s it. If you put this code in a Page_Load method for your own custom page, there’s no need to edit the publish.htm file everytime you deploy a new version. You may even go as far as creating a page that collects multiple ClickOnce deployable solutions. Because we get the input from the actual and updated ClickOnce webpage.