Export (0) Print
Expand All
34 out of 42 rated this helpful - Rate this topic

Certificate Expiration in ClickOnce Deployment

Robin Shahan, MVP
GoldMail, Inc.

Download the code for this article on Code Gallery.

ClickOnce is a method for deploying client and console applications. It allows you to host your application on a web server or network share, and includes the ability to perform incremental updates, either automatically or manually. For more information, check out the MSDN article ClickOnce Deployment Overview.

ClickOnce deployments are signed with either a test certificate or a certificate purchased from a vendor such as Verisign or thawte. Most certificates expire in 1 year. When you change the signing certificate of a ClickOnce deployment, the customers may have to uninstall the application and install the new version with the new signing certificate.

In this article, I’m going to explain when your customers will have to uninstall and reinstall, and when they won’t. I show you how to do that programmatically so it has the least amount of impact possible on your customers, and I’ll relate the best curse words to use when you realize you are going to have to do this.

What happens to your deployment when the certificate expires?

You can not publish an application with an expired certificate, so you will be unable to publish any updates or even a new version without a new certificate. You must purchase a new certificate, create a new test certificate, or extend your current certificate with a new test certificate (more about this later).

After the certificate expires, customers will still be able to install your application, although in most cases the trust dialog will say “Unknown Publisher”, even if the certificate was purchased from a certificate vendor.

According to Microsoft, if your deployment is timestamped, the trust dialog will continue to say “Published by …” and display your credentials. If the deployment is not timestamped, the trust dialog will say “Unknown Publisher”. You can tell if your deployment is timestamped by checking the Signing tab in the properties for your main project. If the Timestamp Server URL is filled in, your deployment is timestamped. For more information about this, check out the MSDN article ClickOnce Deployment and Authenticode.

Why would I have to uninstall and reinstall when I change the signing certificate?

In the past, customers always had to uninstall and reinstall a ClickOnce application when the certificate changed.  This is because signing the deployment with a different certificate changes the public key token, which is part of the deployment identity.

For automatic updates, this results in the dreaded “Application cannot be updated.” error. For programmatic updates, calling the CheckForUpdate API throws a NullReferenceException.

There are several criteria that go into figuring out if your customers will have to uninstall and reinstall your application—the type of updates (manual or automatic), the version of the .NET Framework that you are targeting, the type of application (Office/WinForms/WPF/Console), and maybe even your astrological sign (or so it seems).

If you are using Visual Studio 2010 and targeting .NET 4.0, you should be okay in all cases. It works for Office solutions (a.k.a VSTO), smart client applications, automatic updates, and manual updates.

If you are doing manual updates and you are not targeting the .NET 4.0 Framework, your customers will have to uninstall and reinstall. If you meet all of the other criteria, you could try changing your application to use automatic updates, and then change it back to programmatic updates after all of the customers have upgraded to the new version, but I haven’t tried it, so I don’t know that it would work.

If you are using Visual Studio 2008 and are targeting .NET 3.5 and using automatic updates, you can just change the certificate and deploy a new version, unless you have an Office solution. I discovered a new problem in September 2009—if you have an Office solution and are using Visual Studio 2008, your customers will have to uninstall and reinstall when you change the certificate. This is due to a regression bug in the VSTO Runtime 3.0 SP-1, and I hope it doesn’t surprise you as much as it did me on the day my certificate expired. (Not a good surprise, like a surprise birthday party. More like when your car runs out of oil.)

If the moon is in the 7th sun and you can’t get that Aquarius song out of your head, and you wish your application targeted .NET 3.5 but it doesn’t, your customers will have to uninstall and reinstall.

Is there a fix for this?

Microsoft put out a partial fix for this problem and included it in .NET 2.0 SP-1 and .NET 3.5  for ClickOnce deployments using automatic updates. (Your customers have .NET 2.0 SP-1 if their version is equal to or higher than 2.0.50727.1434).

Unfortunately, Windows Vista was shipped with .NET 2.0.50727.312, and the fix was not included in .NET 3.0, and you cannot install .NET 2.0 SP-1 on Windows Vista. (If you don’t believe me, find a computer running Windows Vista and take a minute and wander over to MSDN and search for .Net 2.0 SP-1 Download and give it a try. I did. I call this “clutching at straws”.) So the only way to upgrade .NET 2.0 on Windows Vista is to install .NET 3.5 or .NET 3.5 SP-1.

Break it to me gently: do I have to uninstall and reinstall?

I can unequivocally say if your application targets .NET 4.0, or your application is not an Office solution and it targets.NET 3.5 and uses automatic updates, or none of your customers have Windows Vista, you can just replace your certificate, issue an update, and go to lunch while the rest of us struggle on.

Like I said before, whether your customers need to uninstall and reinstall when you update your certificate depends on the version of the .NET Framework you are targeting, what version your customers have, whether you are using automatic or programmatic updates, whether you are using a test certificate or one from a certificate vendor. To make this easier for you, I have included a flowchart (Figure 1) to help you figure out if your customers will have to uninstall and reinstall. You can find a PDF version of this chart at the same link as the code samples: http://code.msdn.microsoft.com/ClickOnceCertExp.

Shahan_Figure1.jpg

Figure 1. Decision Flowchart

A Word about Test Certificates vs Purchased Certificates

You can create your own signing certificate with Visual Studio (check out the Signing tab, and that button that says “Create Test Certificate”) or using a program called MakeCert. When you do this, the trust dialog says the publisher is unknown. If you purchase a certificate, the trust dialog will list your company as the publisher. The certificate vendor basically guarantees that you are who you say you are.

My company (GoldMail) uses ClickOnce to deploy our application from a content delivery network to our customers, The Public. We did not want our deployment to say “Unknown Publisher”; we wanted it to say “Published by: GoldMail”. This gives our customers a warm cozy feeling when installing our application from the internet, and provides reassurance that it is coming from our company and not from some guy living in the frozen tundra hacking deployments for fun.

If your application is going to be deployed internally, you might choose to forego the cost of a purchased certificate, because people would know where the application was coming from even if it didn’t specify that on the installation dialogs. You can also choose to install the test certificate on each user’s machine, and the trust dialog will then show the name on the certificate as if it were one purchased from a vendor.

You also might want to use a test certificate if you are deploying your application for specific customers who know you and know the application is coming from you, like if you are writing shareware, or creating an application for your friends and family to use to keep track of what you want for your birthday and Christmas.

And there is one very large benefit to using a test certificate, in terms of the whole expiring-certificate problem…

Test Certificates, MakeCert, RenewCert, and The Big Workaround

You can use MakeCert to create your own certificate and specify the end date so you don’t have to deal with the expired certificate issue until you upgrade to .NET 4.0 and no longer have to deal with it. I will show you how to do this in the next section.

When your test certificate does expire, you can use RenewCert to “copy it forward” and extend the dates. This is The Big Workaround, because it appears to be the same certificate—except for the magical date change—and you can now just use the new certificate to sign your deployment and go to lunch.

You can also use RenewCert to “copy forward” a certificate from a vendor, but it will lose its “trustworthiness” and become a test certificate. You can do this if you are not going to purchase a new certificate, or if your vendor certificate has expired and you must deploy an update. For example, I used RenewCert to deploy an update to our application with the uninstall/reinstall code after our vendor certificate expired.

If you haven’t deployed your application yet, and you are not going to use a purchased certificate, here’s how to make your own long-term test certificate using MakeCert.

If you are not going to purchase a certificate, then you can sign your deployment by using the Signing tab for your project’s Properties. If you go into the Signing tab and click on “Create Test Certificate”, Visual Studio creates one for you and uses it to sign your manifests, as illustrated in Figure 2.

Shahan_Figure2.jpg

Figure 2. Signing your deployment with a test certificate

This creates a test certificate that expires in one year, at which time you will need to deal with the problem of the certificate expiring.

You can create your own certificate using MakeCert.exe and Pvk2Pfx.exe and set the expiration date yourself, and you can view the attributes of the certificate you create using CertMgr.exe. If you are running Windows Vista or Windows Fabulous (aka Windows 7), you will find these files under C:\Program Files\Microsoft SDKs\Windows\v6.0A\bin\. (You might have to install the Windows SDK. If you have Visual Studio 2010 installed then the folder will be \Program Files\Microsoft SDKs\Windows\v7.0A\bin\.)

Makecert creates two files—a pvk (private key) file and a cer (certificate) file. In the following example, the cer file is called MyCertFile; the pvk file contains the private key, and is called MyCert.pvk. This certificate is valid from 3/28/09 to 3/31/12.

Makecert –sv Nightbird.pvk –n “CN=Nightbird Consulting” NightbirdCert.cer –b 03/28/2009 –e 03/31/2012

  • -sv specifies the PVK file, which is created if it doesn’t already exist. This is the private key for your certificate.
  • -n is the certificate X500 name.
  • -b is the begin date in format mm/dd/yyyy.
  • -e is the end date in format mm/dd/yyyy.

If you do not have a pvk file, you will be prompted for a password to use when creating the pvk file. Next, you will be prompted to enter that password to access the pvk file in order to create the cer file.

If you enter the Makecert commands displayed previously, you will have two files: Nightbird.pvk and NightbirdCert.cer. Your next step is to create a pfx file out of these that you can use to sign your deployment in Visual Studio.

Pvk2pfx –pvk Nightbird.pvk –spc NightbirdCert.cer –pfx NightbirdPFX.pfx –po r0b1n

You will be prompted for the password to the private key file; this is the same password you defined in the first step.

  • -pvk is the file containing the private key. This is the NightbirdCert.pvk file created in the previous step.
  • -spc specifies the certificate file created in the previous step.
  • -pfx specifies the name of the PFX file to be created.
  • -po specifies the password for the pfx file. You will be prompted for this password when you add the key to Visual Studio. You should not use the same value as you did for the private key, and you should make this a decent password, not something like your company name or an easily discerned version of your own name (like r0b1n).

If you enter the commands above, you will end up with a certificate file called NightbirdPFX.pfx that is valid through 2012. This is illustrated in Figure 3.

Shahan_Figure3.jpg

Figure 3. Creating a PFX file with MakeCert.

You can now use this to sign your deployment. Click on “Select from File” in the Signing tab under your project’s properties and select the pfx file you just created. The PFX file is added to your project and the certificate is imported into your certificate store. Figure 4 shows what it looks like in Visual Studio:

Shahan_Figure4.jpg

Figure 4. Signing the deployment with the new PFX file

To see the certificate in your local store, run CertMgr.exe that you retrieved from the same folder as MakeCert.exe, and look under the Personal tab for your new certificate. This brings up the application as displayed in Figure 5.

Shahan_Figure5.jpg

Figure 5. Your certificate in the certificate store

If you double-click on your new certificate, you can see the details as shown in Figure 6.

Shahan_Figure6.jpg

Figure 6. Certificate Details

The certificate cannot be verified because you created it, rather than purchasing it from a certificate vendor. This is why your users see “Unknown publisher” as the publisher when you use this file to sign your deployment. For more information, see the MSDN article ClickOnce Deployment and Authenticode for an explanation of how using Certificate Authorities helps users.

This certificate does not expire for 3 years. If you are going to use a test certificate, you might as well create one using MakeCert and PVK2PFX that lasts for longer than one year. Heck, make it last 10 years, and this won’t be an issue unless your application lasts a really long time, by which time Microsoft will have completely fixed the problem because we will all be running .NET 13.7.

However, if you are going to install the test certificate in the user’s certificate store (so you appear to be trusted), having it last more than a year is not recommended. You cannot revoke these certificates, which means if someone else obtained your certificate, they could deploy malware on the computer of anyone with that certificate installed in their certificate store and it would be marked as coming from a trusted publisher (you). For enough information about certificate revocation to make your eyes cross, check out the TechNet article Certificate Revocation and Status Checking.

What do I do if my certificate has expired?

So you have already deployed your application, and now your certificate (purchased or unpurchased) has expired, and you’ve examined the flowchart and determined that your customers are going to have to uninstall and reinstall the application. You can’t even issue an update. Visual Studio will not let you deploy your application with an expired certificate. So what do you do now?

If you need to extend an existing certificate, you can use a program called RenewCert. For details, you can check out my blog post How to extend an existing certificate, even if it has expired.

You can also find a version of RenewCert code on MSDN. I have not tested that specific version, but I’ve heard that it works with test certificates but not purchased certificates. Here’s the link if you want to check it out: http://support.microsoft.com/kb/925521

If you are already using a test certificate, extending it solves your problem. You can sign your deployment with the extended certificate, issue updates, and it will work fine. You can go to lunch, and the rest of us with known publishers can eat at our desks while we continue on. (Can you bring something back for us?)

If you are using a purchased certificate and it has expired, you can use an extended certificate to sign and deploy an update to your application, but it will look like a test certificate. This will seem just like any other update to your customer who already has the application installed, because it does not show the trust dialog when installing an update. New customers will see “Unknown Publisher” in the trust dialog because you are now using a test certificate.

So if your purchased certificate has expired, this enables you to issue an update to the application that programmatically uninstalls the current version and installs a new version signed with the new purchased certificate.

How to programmatically uninstall a Click Once application and install a new version

I am assuming here that either your certificate has not expired yet, or you have created a new extended version of your certificate, as discussed in the previous section.

Here are the next steps:

  1. I am assuming you already have a version of your application deployed. For testing, go ahead and deploy a version with the old signing certificate (or with the test certificate you created with the same public/private key pair using RenewCert). Let’s call this version 1.1.0.0. This should be a copy of the production version of your application.
  2. Next, deploy a version of your application using your new purchased certificate to a different URL than the current version of your application. This is the new version that will be installed. Let’s call this version 1.3.0.0 (it just needs to be a higher version than the update you deploy in step 3).
  3. Add code to your current application that uninstalls itself, and then starts the new installation from the new URL. Deploy this as a update to the current deployment (set the minimum version in the Updates dialog equal to the version number of this release). Let’s call this version 1.2.0.0.

Your users will click the shortcut to run the application they already have installed (version 1.1.0.0). It will perform the update to 1.2.0.0 and then run that version, which will uninstall itself and install version 1.3.0.0 from the new URL.

The third code sample (CertificateExpirationSample_3_UninstallVersion in the code download) contains the uninstall/reinstall code. Let’s take a look at the particulars. DeploymentUtilsWin32 is the code to search for a window and press a button. Just take it on faith. It’s not terribly interesting anyway. Let’s go straight to DeploymentUtils instead.

DeploymentUtils

This is the routine with the actual Uninstall code. You call this from your program startup. This gets the uninstall string from the registry and runs it. When you uninstall a ClickOnce application, you get a dialog that asks if it is okay to uninstall the application. This code presses that button programmatically. If you have any customers who are really fast with the mouse, you might want to warn them. If they DO click the OK button, it uninstalls, but doesn’t install the new version.

  'VB
  ''' <summary>
  ''' Uninstall the current version of the application.
  ''' </summary>
  Public Shared Sub UninstallMe()
    Dim publicKeyToken As String = GetPublicKeyToken()
    'Find Uninstall string in registry
    Dim DisplayName As String = Nothing
    Dim uninstallString As String = _
      GetUninstallString(publicKeyToken, DisplayName)
    If (uninstallString.Length <= 0) Then
      Return
    End If

    Dim runDLL32 As String = uninstallString.Substring(0, 12)
    Dim args As String = uninstallString.Substring(13)

    'start the uninstall; this will bring up the uninstall dialog
    '  asking if it's ok
    Dim uninstallProcess As Process = Process.Start(runDLL32, args)

    'push the OK button
    PushUninstallOKButton(DisplayName)
  End Sub

  //C#
  /// <summary>
  /// Uninstall the current version of the application.
  /// </summary>
  public static void UninstallMe()
  {
      string publicKeyToken = GetPublicKeyToken();
      // Find Uninstall string in registry
      string DisplayName = null;
      string uninstallString = GetUninstallString(publicKeyToken,
        out DisplayName);
      if (uninstallString.Length <= 0)
      {
          return;
      }

      string runDLL32 = uninstallString.Substring(0, 12);
      string args = uninstallString.Substring(13);

      //start the uninstall; this will bring up the uninstall dialog
      //  asking if it's ok
      Process uninstallProcess = Process.Start(runDLL32, args);

      //push the OK button
      PushUninstallOKButton(DisplayName);
  }

GetPublicKeyToken does exactly that, for the current running application:

  'VB
  ''' <summary>
  ''' Gets the public key token for the current running
  ''' (ClickOnce) application.
  ''' </summary>
  ''' <returns></returns>
  Public Shared Function GetPublicKeyToken() As String
    Dim asi As ApplicationSecurityInfo = _
      New ApplicationSecurityInfo(AppDomain.CurrentDomain.ActivationContext)
    Dim pk As Byte() = asi.ApplicationId.PublicKeyToken
    Dim pkt As StringBuilder = New StringBuilder()
    For i As Integer = 0 To pk.GetLength(0) - 1 Step 1
      pkt.Append(String.Format("{0:x2}", pk(i)))
    Next
    Return pkt.ToString()
  End Function

  //C#
  /// <summary>
  /// Gets the public key token
  /// for the current running (ClickOnce) application.
  /// </summary>
  /// <returns></returns>
  public static string GetPublicKeyToken()
  {
      ApplicationSecurityInfo asi = new
ApplicationSecurityInfo(AppDomain.CurrentDomain.ActivationContext);
      byte[] pk = asi.ApplicationId.PublicKeyToken;
      StringBuilder pkt = new StringBuilder();
      for (int i = 0; i < pk.GetLength(0); i++)
          pkt.Append(String.Format("{0:x2}", pk[i]));
      return pkt.ToString();
  }

GetUninstallString looks in the registry and searches the uninstall strings for ClickOnce applications, looking for a match to the public key token and the application name. If found, it returns the uninstall string and the Display Name (used to find the window in order to press the OK button). Here is an example of what the uninstall string will look like, with the public key token replaced with x characters:

rundll32.exe dfshim.dll,ShArpMaintain GMStudio.application, Culture=neutral, PublicKeyToken=xxxxxxxxxxxxxxxx, processorArchitecture=msil

And here’s the code.

  'VB
  ''' <summary>
  ''' Gets the uninstall string for the current ClickOnce app
  ''' from the Windows Registry.
  ''' </summary>
  ''' <param name="PublicKeyToken">The public key token of the app.</param>
  ''' <returns>The command line to execute
  ''' that will uninstall the app.</returns>
  Public Shared Function GetUninstallString(ByVal PublicKeyToken As String, _
   ByRef DisplayName As String) As String

    Dim uninstallString As String = Nothing

    'set up the string to search for
    Dim searchString As String = "PublicKeyToken=" & PublicKeyToken

    'open the registry key and get the subkey names
    Dim uninstallKey As RegistryKey = _
      Registry.CurrentUser.OpenSubKey("Software\Microsoft\" & _
      "Windows\CurrentVersion\Uninstall")
    Dim appKeyNames As String() = uninstallKey.GetSubKeyNames()

    DisplayName = Nothing
    Dim found As Boolean = False

    ''search through the list for one with a match
    For Each appKeyName As String In appKeyNames

      Dim appKey As RegistryKey = uninstallKey.OpenSubKey(appKeyName)
      uninstallString = appKey.GetValue("UninstallString").ToString()
      DisplayName = appKey.GetValue("DisplayName").ToString
      appKey.Close()
      'look for the public key token, and the display name
      '(same as ProductName in the ClickOnce properties)
      If (uninstallString.Contains(PublicKeyToken) _
       AndAlso DisplayName = "TestCertExp_VB") Then
        found = True
        Exit For
      End If

    Next

    uninstallKey.Close()

    If (found) Then
      Return uninstallString
    Else
      Return String.Empty
    End If

  End Function

  //C#
  /// <summary>
  /// Gets the uninstall string for the current ClickOnce app
  /// from the Windows Registry.
  /// </summary>
  /// <param name="PublicKeyToken">The public key token of the app.</param>
  /// <returns>The command line to execute that will
  /// uninstall the app.</returns>
  public static string GetUninstallString(string PublicKeyToken,
    out string DisplayName)
  {
      string uninstallString = null;

      //set up the string to search for
      string searchString = "PublicKeyToken=" + PublicKeyToken;

      //open the registry key and get the subkey names
      RegistryKey uninstallKey =
        Registry.CurrentUser.OpenSubKey("Software\\Microsoft" +
        "\\Windows\\CurrentVersion\\Uninstall");
      string[] appKeyNames = uninstallKey.GetSubKeyNames();

      DisplayName = null;
      bool found = false;

      //search through the list for one with a match
      foreach (string appKeyName in appKeyNames)
      {
          RegistryKey appKey = uninstallKey.OpenSubKey(appKeyName);
          uninstallString = (string)appKey.GetValue("UninstallString");
          DisplayName = (string)appKey.GetValue("DisplayName");
          appKey.Close();
          if (uninstallString.Contains(PublicKeyToken)
            && DisplayName == "TestCertExp_CSharp")
          {
              found = true;
              break;
          }
      }

      uninstallKey.Close();

      if (found)
          return uninstallString;
      else
          return string.Empty;
  }

To press the OK button, you have to search through the windows for the right one, and then search the window for the right control. Then you push the button.

  'VB
  ''' <summary>
  ''' Find and Push the OK button on the uninstall dialog.
  ''' </summary>
  ''' <param name="DisplayName">Display Name value from the registry</param>
  Private Shared Sub PushUninstallOKButton(ByVal DisplayName As String)
    Dim success As Boolean = False

    ''Find the uninstall dialog.
    Dim uninstallerWin As IntPtr = _
      FindUninstallerWindow(DisplayName, success)
    Dim OKButton As IntPtr = IntPtr.Zero

    ''If it found the window, look for the button.
    If (success) Then
      OKButton = FindUninstallerOKButton(uninstallerWin, success)
    End If

    ''If it found the button, press it.
    If (success) Then
      DeploymentUtilsWin32.DoButtonClick(OKButton)
    End If
  End Sub

  //C#
  /// <summary>
  /// Find and Push the OK button on the uninstall dialog.
  /// </summary>
  /// <param name="DisplayName">Display Name value from the registry</param>
  private static void PushUninstallOKButton(string DisplayName)
  {
      bool success = false;

      //Find the uninstall dialog.
      IntPtr uninstallerWin =
        FindUninstallerWindow(DisplayName, out success);
      IntPtr OKButton = IntPtr.Zero;

      //If it found the window, look for the button.
      if (success)
          OKButton = FindUninstallerOKButton(uninstallerWin, out success);

      //If it found the button, press it.
      if (success)
          DeploymentUtilsWin32.DoButtonClick(OKButton);
  }

This code looks for the Uninstall Dialog.It searches 25 times, because it may take a few seconds for ClickOnce to display the dialog.

   'VB
  ''' <summary>
  ''' Find the uninstall dialog.
  ''' </summary>
  ''' <param name="DisplayName">Display Name retrieved
  ''' from the registry.</param>
  ''' <param name="success">Whether the window was found or not.</param>
  ''' <returns>Pointer to the uninstall dialog.</returns>
  Private Shared Function FindUninstallerWindow(ByVal DisplayName As String,_
   ByRef success As Boolean) As IntPtr
    ''Max number of times to look for the window,
    ''used to let you out if there's a problem.
    Dim i As Integer = 25
    Dim w32 As New DeploymentUtilsWin32()
    Dim uninstallerWindow As IntPtr = IntPtr.Zero
    Do While (uninstallerWindow = IntPtr.Zero AndAlso i > 0)
      uninstallerWindow = _
        w32.SearchForTopLevelWindow(DisplayName + " Maintenance")
      System.Threading.Thread.Sleep(500)
      i -= 1
    Loop
    If (uninstallerWindow = IntPtr.Zero) Then
      success = False
    Else
      success = True
    End If

    Return uninstallerWindow
  End Function

  //C#
  /// <summary>
  /// Find the uninstall dialog.
  /// </summary>
  /// <param name="DisplayName">Display Name retrieved
  /// from the registry.</param>
  /// <param name="success">Whether the window was found or not.</param>
  /// <returns>Pointer to the uninstall dialog.</returns>
  private static IntPtr FindUninstallerWindow(string DisplayName,
    out bool success)
  {
      //Max number of times to look for the window,
      //used to let you out if there's a problem.
      int i = 25;
      DeploymentUtilsWin32 w32 = new DeploymentUtilsWin32();
      IntPtr uninstallerWindow = IntPtr.Zero;
      while (uninstallerWindow == IntPtr.Zero && i > 0)
      {
          uninstallerWindow = w32.SearchForTopLevelWindow(DisplayName +
            " Maintenance");
          System.Threading.Thread.Sleep(500);
          i--;
      }

      if (uninstallerWindow == IntPtr.Zero)
          success = false;
      else
          success = true;

      return uninstallerWindow;
  }

This code looks for the OK button on the uninstall Dialog. Again, it searches 25 times, just to be thorough. It’s not unlike looking for your car keys in the same place 4 times, only to have them show up the last time.

  'VB
  ''' <summary>
  ''' Find the OK button on the uninstall dialog.
  ''' </summary>
  ''' <param name="UninstallerWindow">The pointer to
  ''' the Uninstall Dialog</param>
  ''' <param name="success">Whether it succeeded or not.</param>
  ''' <returns>A pointer to the OK button</returns>
  Private Shared Function FindUninstallerOKButton(ByVal UninstallerWindow _
   As IntPtr, ByRef success As Boolean) As IntPtr
    ''max number of times to look for the button,
    ''lets you out if there's a problem
    Dim i As Integer = 25
    Dim w32 As New DeploymentUtilsWin32()
    Dim OKButton As IntPtr = IntPtr.Zero

    Do While (OKButton = IntPtr.Zero AndAlso i > 0)
      OKButton = w32.SearchForChildWindow(UninstallerWindow, "&OK")
      System.Threading.Thread.Sleep(500)
      i -= 1
    Loop

    If (OKButton = IntPtr.Zero) Then
      success = False
    Else
      success = True
    End If

    Return OKButton
  End Function

  //C#
  /// <summary>
  /// Find the OK button on the uninstall dialog.
  /// </summary>
  /// <param name="UninstallerWindow">The pointer to
  /// the Uninstall Dialog</param>
  /// <param name="success">Whether it succeeded or not.</param>
  /// <returns>A pointer to the OK button</returns>
  private static IntPtr FindUninstallerOKButton(IntPtr UninstallerWindow,
    out bool success)
  {
      //max number of times to look for the button,
      //lets you out if there's a problem
      int i = 25;
      DeploymentUtilsWin32 w32 = new DeploymentUtilsWin32();
      IntPtr OKButton = IntPtr.Zero;

      while (OKButton == IntPtr.Zero && i > 0)
      {
          OKButton = w32.SearchForChildWindow(UninstallerWindow, "&OK");
          System.Threading.Thread.Sleep(500);
          i--;
      }

      if (OKButton == IntPtr.Zero)
          success = false;
      else
          success = true;

      return OKButton;
  }

Now you need code to install the new version. You will have to hardcode the publishing location for the new version in this routine.

  'VB
  ''' <summary>
  ''' Install the new version of the application from a different URL.
  ''' Then exit this version of the application.
  ''' Trigger this specifically with Internet Explorer
  '''   in case the user has their default browser set to something else.
  ''' This eliminates any problems running it with Firefox.
  ''' If you are changing something like the target CPU, or installing
  '''   a new prerequisite, run setup.exe.
  ''' Otherwise, you can just call the application fle directly,
  '''   because the user won't have the application installed w/o the
  '''   current prerequisites.
  ''' </summary>
  Public Shared Sub InstallNewVersion()
    Dim url As String = _
      "http://localhost/NewVersion/TestCertExp_VB.application"
    System.Diagnostics.Process.Start("iexplore.exe", url)
    System.Windows.Forms.Application.Exit()
    Return
  End Sub

  //C#
  public static void InstallNewVersion()
  {
      string url =
        @"http://localhost/NewVersion/TestCertExp_CSharp.application";
      System.Diagnostics.Process.Start("iexplore.exe", url);
      System.Windows.Forms.Application.Exit();
      return;
  }

And last, you need to call these routines in your startup. If you’re using C#, this is probably in Program.cs. In VB, it would go in the Application Startup Event. You can see this in the VB version of the code samples provided. For more information on Application Events in VB, see the MSDN article How to: Run Code When the Application Starts or Ends.

Give the users a little information using a MessageBox, so they know what’s about to happen.

When I used this code, I actually let the customers run the old version once before forcing the update, in case they were trying to do something Very Important and didn’t want to take time to do the update Right That Second. You do have some flexibility, but I wouldn’t let them run the old version forever.

  //C#
  static class Program
  {
      /// <summary>
      /// The main entry point for the application.
      /// </summary>
      [STAThread]
      static void Main()
      {
          Application.EnableVisualStyles();
          Application.SetCompatibleTextRenderingDefault(false);

          MessageBox.Show(
             "The current version of this application “
              + “\r\nwill now be uninstalled for you,”
              + “\r\nand then the new version will be installed.",
              "Upgrade Notification", MessageBoxButtons.OK,
              MessageBoxIcon.Information, MessageBoxDefaultButton.Button1);

          DeploymentUtils.UninstallMe();
          DeploymentUtils.InstallNewVersion();
      }
       //Application.Run(new MainForm()); //we don’t need this any more!
  }

And that should take care of that. You simply deploy the version with the uninstall/install code to the same publishing location as the current deployment. It must be a forced update, so in the Update dialog, set the minimum version equal to the same as the version number you are deploying.

What happens to my customer’s data?

If your customers have to uninstall and reinstall manually, they will lose any data that you might be storing in the ClickOnce DataDirectory.

When uninstalling and reinstalling programmatically, if you don’t specifically handle data stored in the ClickOnce cache, it will be lost forever.

For a different perspective on how to deploy data in a ClickOnce application and where you might want to put it for safekeeping, check out my article Where do I put my data to keep it safe from ClickOnce updates?.

In the uninstall/reinstall application, you could move the data, and in the new version, you could look for it in the new location.

Other methods

I’ve seen methods that double-sign the deployment using the signtool from Windows Server 2003, but I highly discourage you from going this route because it is not supported by either Visual Studio or Mage, and I don’t believe it will work anymore.

Other uses for the uninstall/reinstall code

The method for uninstalling and reinstalling the ClickOnce application programmatically has other uses.

If you change anything about the application identity, your customers have to uninstall and reinstall. I took advantage of our uninstall/reinstall to change the Target CPU for our application as well as the assembly name.

You could also use this methodology to force a reinstall when you change the prerequisites of your application, such as upgrading from one version of the .NET Framework to another. To do that, simply have the reinstall code call “setup.exe” instead of “YourAppName.application.”

And you could use it if you wanted to change the URL of your deployment, or just to make your customers think they’re getting a huge new upgrade.

This is great for Windows Forms, WPF, and Console Applications; what about Office solutions?

As noted in the excellent flowchart in Figure 1, if you have an Office solution and you are using VS2008 with the VSTO Runtime 3.0 SP1, your customers will have to uninstall and reinstall when you change your signing certificate. I am trying to figure out if that can be done programmatically from within the add-in itself. When I come up with a working solution, I will add it to the code samples.

Conclusion

This article explained how to recognize and deal with the issue of expiring certificates, depending on the version of the .NET Framework you are targeting, what kind of application you are deploying, and what kind of certificate you are using, among other factors. It even included a flowchart which displayed my proficiency with Microsoft Visio 2007. ;-)

Expiring certificates wouldn’t be a problem if Microsoft would install .NET 2.0 SP1 on Windows Vista, but  unfortunately there is a huge amount of risk and no small amount of difficulty patching the .NET 2.0 Framework on .NET 3.0 Vista. The most effective course of action is to upgrade to VS2010 and target .NET 4.0.

If you are using VS2008, the most effective course of action for Windows Forms, WPF, and console applications is to target.NET 3.5 SP1 and use automatic updates. If the size of .NET 3.5 SP1 is keeping you from updating, it might help to know .NET 3.5 SP1 is now being deployed by Microsoft via Windows Update as a critical update as long as there is any version of .NET on the machine (and the user has windows updates turned on). So over time, this will become a non-issue. Unless you’re doing manual updates, and then you will have to wait for .NET 4.0.

If size really matters to you, you should know that Microsoft has greatly reduced the size of both the Full and Client Profile versions of the .NET 4.0 Framework. At the time of this writing, the Client Profile is around 30MB and the full version is around 40MB. You don’t need higher math to figure out this is much smaller than .NET 3.5, which I believe is around 80MB. For more information, check out this .NET blog entry: What’s new in .NET Framework 4 Client Profile Beta 2.

If you have any questions or would like to watch some really cool GoldMails (similar to videos) explaining other common questions about ClickOnce deployment, please check out my blog.

You can post your ClickOnce problems in the MSDN ClickOnce and Setup & Deployment Forum.

References and thanks to the following

Did you find this helpful?
(1500 characters remaining)
Thank you for your feedback
Show:
© 2014 Microsoft. All rights reserved.