How to initiate an uninstall of a ClickOnce app, from within the app?

12,379

Solution 1

I'd recommend checking out this MSDN article here. It explains how to programmatically uninstall an app (and reinstall from a new URL if you want to):

http://msdn.microsoft.com/en-us/library/ff369721.aspx

This is a variant on the jameshart blog entry, but it includes a couple of fixes that you're going to want to use. There are code downloads in both C# and VB.

In fact, you can just push an update and have the app uninstall itself, you don't even need the user to say "ok".

Solution 2

I'll just leave this here for anyone that comes looking for code, and finds that the download links in the other answers are dead:

https://code.google.com/p/clickonce-application-reinstaller-api

Edit: Added the code from Reinstaller.cs and instructions from ReadMe.txt

/* ClickOnceReinstaller v 1.0.0
 *  - Author: Richard Hartness ([email protected])
 *  - Project Site: http://code.google.com/p/clickonce-application-reinstaller-api/
 * 
 * Notes:
 * This code has heavily borrowed from a solution provided on a post by
 * RobinDotNet (sorry, I couldn't find her actual name) on her blog,
 * which was a further improvement of the code posted on James Harte's
 * blog.  (See references below)
 * 
 * This code contains further improvements on the original code and
 * wraps it in an API which you can include into your own .Net, 
 * ClickOnce projects.
 * 
 * See the ReadMe.txt file for instructions on how to use this API.
 * 
 * References:
 * RobinDoNet's Blog Post:
 * - ClickOnce and Expiring Certificates
 *   http://robindotnet.wordpress.com/2009/03/30/clickonce-and-expiring-certificates/
 *   
 * Jim Harte's Original Blog Post:
 * - ClickOnce and Expiring Code Signing Certificates
 *   http://www.jamesharte.com/blog/?p=11
 */


using Microsoft.Win32;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Deployment.Application;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Policy;
using System.Windows.Forms;
using System.Xml;

namespace ClickOnceReinstaller
{
    #region Enums
    /// <summary>
    /// Status result of a CheckForUpdates API call.
    /// </summary>
    public enum InstallStatus { 
        /// <summary>
        /// There were no updates on the server or this is not a ClickOnce application.
        /// </summary>
        NoUpdates, 
        /// <summary>
        /// The installation process was successfully executed.
        /// </summary>
        Success, 
        /// <summary>
        /// In uninstall process failed.
        /// </summary>
        FailedUninstall, 
        /// <summary>
        /// The uninstall process succeeded, however the reinstall process failed.
        /// </summary>
        FailedReinstall };
    #endregion

    public static class Reinstaller
    {
        #region Public Methods

        /// <summary>
        /// Check for reinstallation instructions on the server and intiate reinstallation.  Will look for a "reinstall" response at the root of the ClickOnce application update address.
        /// </summary>
        /// <param name="exitAppOnSuccess">If true, when the function is finished, it will execute Environment.Exit(0).</param>
        /// <returns>Value indicating the uninstall and reinstall operations successfully executed.</returns>
        public static InstallStatus CheckForUpdates(bool exitAppOnSuccess)
        {
            //Double-check that this is a ClickOnce application.  If not, simply return and keep running the application.
            if (!ApplicationDeployment.IsNetworkDeployed) return InstallStatus.NoUpdates;

            string reinstallServerFile = ApplicationDeployment.CurrentDeployment.UpdateLocation.ToString();

            try
            {
                reinstallServerFile = reinstallServerFile.Substring(0, reinstallServerFile.LastIndexOf("/") + 1);
                reinstallServerFile = reinstallServerFile + "reinstall";
#if DEBUG
                Trace.WriteLine(reinstallServerFile);

#endif          
            } 
            catch 
            {
                return InstallStatus.FailedUninstall;
            }
            return CheckForUpdates(exitAppOnSuccess, reinstallServerFile);
        }

        /// <summary>
        /// Check for reinstallation instructions on the server and intiate reinstall.
        /// </summary>
        /// <param name="exitAppOnSuccess">If true, when the function is finished, it will execute Environment.Exit(0).</param>
        /// <param name="reinstallServerFile">Specify server address for reinstallation instructions.</param>
        /// <returns>InstallStatus state of reinstallation process.</returns>
        public static InstallStatus CheckForUpdates(bool exitAppOnSuccess, string reinstallServerFile)
        {
            string newAddr = "";

            if (!ApplicationDeployment.IsNetworkDeployed) return InstallStatus.NoUpdates;

            //Check to see if there is a new installation.
            try
            {
                HttpWebRequest rqHead = (HttpWebRequest)HttpWebRequest.Create(reinstallServerFile);
                rqHead.Method = "HEAD";
                rqHead.Credentials = CredentialCache.DefaultCredentials;
                HttpWebResponse rsHead = (HttpWebResponse)rqHead.GetResponse();

#if DEBUG
                Trace.WriteLine(rsHead.Headers.ToString());
#endif
                if (rsHead.StatusCode != HttpStatusCode.OK) return InstallStatus.NoUpdates;

                //Download the file and extract the new installation location
                HttpWebRequest rq = (HttpWebRequest)HttpWebRequest.Create(reinstallServerFile);
                WebResponse rs = rq.GetResponse();
                Stream stream = rs.GetResponseStream();
                StreamReader sr = new StreamReader(stream);

                //Instead of reading to the end of the file, split on new lines.
                //Currently there should be only one line but future options may be added.  
                //Taking the first line should maintain a bit of backwards compatibility.
                newAddr = sr.ReadToEnd()
                    .Split(new string[] {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries)[0];

                //No address, return as if there are no updates.
                if (newAddr == "") return InstallStatus.NoUpdates;
            }
            catch
            {
                //If we receive an error at this point in checking, we can assume that there are no updates.
                return InstallStatus.NoUpdates;
            }


            //Begin Uninstallation Process
            MessageBox.Show("There is a new version available for this application.  Please click OK to start the reinstallation process.");

            try
            {
                string publicKeyToken = GetPublicKeyToken();
#if DEBUG
                Trace.WriteLine(publicKeyToken);
#endif

                // Find Uninstall string in registry    
                string DisplayName = null;
                string uninstallString = GetUninstallString(publicKeyToken, out DisplayName);
                if (uninstallString == null || uninstallString == "") 
                    throw new Exception("No uninstallation string was found.");
                string runDLL32 = uninstallString.Substring(0, uninstallString.IndexOf(" "));
                string args = uninstallString.Substring(uninstallString.IndexOf(" ") + 1);

#if DEBUG
                Trace.WriteLine("Run DLL App: " + runDLL32);
                Trace.WriteLine("Run DLL Args: " + args);
#endif
                Process uninstallProcess = Process.Start(runDLL32, args);
                PushUninstallOKButton(DisplayName);
            }
            catch
            {
                return InstallStatus.FailedUninstall;
            }

            //Start the re-installation process
#if DEBUG
            Trace.WriteLine(reinstallServerFile);
#endif

            try
            {
#if DEBUG
                Trace.WriteLine(newAddr);
#endif
                //Start with IE-- other browser will certainly fail.
                Process.Start("iexplore.exe", newAddr);             
            }
            catch
            {
                return InstallStatus.FailedReinstall;
            }

            if (exitAppOnSuccess) Environment.Exit(0);
            return InstallStatus.Success;
        }
        #endregion

        #region Helper Methods
        //Private Methods
        private 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();
        }
        private static string GetUninstallString(string PublicKeyToken, out string DisplayName)
        {
            string uninstallString = null;
            string searchString = "PublicKeyToken=" + PublicKeyToken;
#if DEBUG
            Trace.WriteLine(searchString);
#endif
            RegistryKey uninstallKey = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall");
            string[] appKeyNames = uninstallKey.GetSubKeyNames();
            DisplayName = null;
            foreach (string appKeyName in appKeyNames)
            {
                RegistryKey appKey = uninstallKey.OpenSubKey(appKeyName);
                string temp = (string)appKey.GetValue("UninstallString");
                DisplayName = (string)appKey.GetValue("DisplayName");
                appKey.Close();
                if (temp.Contains(searchString))
                {
                    uninstallString = temp;
                    DisplayName = (string)appKey.GetValue("DisplayName");
                    break;
                }
            }
            uninstallKey.Close();
            return uninstallString;
        }
        #endregion

        #region Win32 Interop Code
        //Structs
        [StructLayout(LayoutKind.Sequential)]
        private struct FLASHWINFO
        {
            public uint cbSize;
            public IntPtr hwnd;
            public uint dwFlags;
            public uint uCount;
            public uint dwTimeout;
        }

        //Interop Declarations
        [DllImport("user32.Dll")]
        private static extern int EnumWindows(EnumWindowsCallbackDelegate callback, IntPtr lParam);
        [DllImport("User32.Dll")]
        private static extern void GetWindowText(int h, StringBuilder s, int nMaxCount);
        [DllImport("User32.Dll")]
        private static extern void GetClassName(int h, StringBuilder s, int nMaxCount);
        [DllImport("User32.Dll")]
        private static extern bool EnumChildWindows(IntPtr hwndParent, EnumWindowsCallbackDelegate lpEnumFunc, IntPtr lParam);
        [DllImport("User32.Dll")]
        private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
        [DllImport("user32.dll")]
        private static extern short FlashWindowEx(ref FLASHWINFO pwfi);
        [DllImport("user32.dll", SetLastError = true)]
        private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

        //Constants
        private const int BM_CLICK = 0x00F5;
        private const uint FLASHW_ALL = 3;
        private const uint FLASHW_CAPTION = 1;
        private const uint FLASHW_STOP = 0;
        private const uint FLASHW_TIMER = 4;
        private const uint FLASHW_TIMERNOFG = 12;
        private const uint FLASHW_TRAY = 2;
        private const int FIND_DLG_SLEEP = 200; //Milliseconds to sleep between checks for installation dialogs.
        private const int FIND_DLG_LOOP_CNT = 50; //Total loops to look for an install dialog. Defaulting 200ms sleap time, 50 = 10 seconds.

        //Delegates
        private delegate bool EnumWindowsCallbackDelegate(IntPtr hwnd, IntPtr lParam);

        //Methods
        private static IntPtr SearchForTopLevelWindow(string WindowTitle)
        {
            ArrayList windowHandles = new ArrayList();
            /* Create a GCHandle for the ArrayList */
            GCHandle gch = GCHandle.Alloc(windowHandles);
            try
            {
                EnumWindows(new EnumWindowsCallbackDelegate(EnumProc), (IntPtr)gch);
                /* the windowHandles array list contains all of the
                    window handles that were passed to EnumProc.  */
            }
            finally
            {
                /* Free the handle */
                gch.Free();
            }

            /* Iterate through the list and get the handle thats the best match */
            foreach (IntPtr handle in windowHandles)
            {
                StringBuilder sb = new StringBuilder(1024);
                GetWindowText((int)handle, sb, sb.Capacity);
                if (sb.Length > 0)
                {
                    if (sb.ToString().StartsWith(WindowTitle))
                    {
                        return handle;
                    }
                }
            }

            return IntPtr.Zero;
        }
        private static IntPtr SearchForChildWindow(IntPtr ParentHandle, string Caption)
        {
            ArrayList windowHandles = new ArrayList();
            /* Create a GCHandle for the ArrayList */
            GCHandle gch = GCHandle.Alloc(windowHandles);
            try
            {
                EnumChildWindows(ParentHandle, new EnumWindowsCallbackDelegate(EnumProc), (IntPtr)gch);
                /* the windowHandles array list contains all of the
                    window handles that were passed to EnumProc.  */
            }
            finally
            {
                /* Free the handle */
                gch.Free();
            }

            /* Iterate through the list and get the handle thats the best match */
            foreach (IntPtr handle in windowHandles)
            {
                StringBuilder sb = new StringBuilder(1024);
                GetWindowText((int)handle, sb, sb.Capacity);
                if (sb.Length > 0)
                {
                    if (sb.ToString().StartsWith(Caption))
                    {
                        return handle;
                    }
                }
            }

            return IntPtr.Zero;

        }
        private static bool EnumProc(IntPtr hWnd, IntPtr lParam)
        {
            /* get a reference to the ArrayList */
            GCHandle gch = (GCHandle)lParam;
            ArrayList list = (ArrayList)(gch.Target);
            /* and add this window handle */
            list.Add(hWnd);
            return true;
        }
        private static void DoButtonClick(IntPtr ButtonHandle)
        {
            SendMessage(ButtonHandle, BM_CLICK, IntPtr.Zero, IntPtr.Zero);
        }
        private static IntPtr FindDialog(string dialogName)
        {
            IntPtr hWnd = IntPtr.Zero;

            int cnt = 0;
            while (hWnd == IntPtr.Zero && cnt++ != FIND_DLG_LOOP_CNT)
            {
                hWnd = SearchForTopLevelWindow(dialogName);
                System.Threading.Thread.Sleep(FIND_DLG_SLEEP);
            }

            if (hWnd == IntPtr.Zero) 
                throw new Exception(string.Format("Installation Dialog \"{0}\" not found.", dialogName));
            return hWnd;
        }
        private static IntPtr FindDialogButton(IntPtr hWnd, string buttonText)
        {
            IntPtr button = IntPtr.Zero;
            int cnt = 0;
            while (button == IntPtr.Zero && cnt++ != FIND_DLG_LOOP_CNT)
            {
                button = SearchForChildWindow(hWnd, buttonText);
                System.Threading.Thread.Sleep(FIND_DLG_SLEEP);
            }
            return button;
        }
        private static bool FlashWindowAPI(IntPtr handleToWindow)
        {
            FLASHWINFO flashwinfo1 = new FLASHWINFO();
            flashwinfo1.cbSize = (uint)Marshal.SizeOf(flashwinfo1);
            flashwinfo1.hwnd = handleToWindow;
            flashwinfo1.dwFlags = 15;
            flashwinfo1.uCount = uint.MaxValue;
            flashwinfo1.dwTimeout = 0;
            return (FlashWindowEx(ref flashwinfo1) == 0);
        }

        //These are the only functions that should be called above.
        private static void PushUninstallOKButton(string DisplayName)
        {
            IntPtr diag = FindDialog(DisplayName + " Maintenance");
            IntPtr button = FindDialogButton(diag, "&OK");
            DoButtonClick(button);
        }
        #endregion
    }
}

Instructions from ReadMe.txt:

A. Referencing this API in current applications.

Follow these instructions to prepare your application for a future application reinstallation from a different install point. These steps add the necessary library references so that your application can automatically reinstall from a new location.
These steps can be followed at any point, even if a new installation is not yet necessary.

  1. Open the ClickOnceReinstaller project and build the project in Release mode.

  2. Open your ClickOnce application and a reference to the ClickOnceReinstaller.dll file to your start up project.

    Alternatively, you can add the ClickOnceReinstaller project to your application and refrence the project.

  3. Next, open the code file containing the entry point for your application. (Typically, in C#, this is Program.cs)

    From within the application entry point file, make a call to the Reinstaller.CheckForUpdates() function. There are a couple of method signatures for CheckForUpdates(). See the Intellisense descriptions for determining which signature to call for your application. Initially, this should not matter because the necessary look-up file should not be published to your installation server.

    (OPTIONAL) The Reinstaller.CheckForUpdates method returns an InstallStatus object which is an enumerated value of the state of the installation process. Capture this value and handle it accordingly. The definitions for each potential return value can be found through Intellisense for each value.

    A NoUpdates response means that there are currently no new updates require a reinstallation of your application.

  4. Test compile your application and republish a new version of the application to the installation server.

B. Updating your application from a new Install location

These steps are required once an application needs to move to a new web address or a change needs to be made to the application requiring the reinstallation of the application.

If your web server needs to move to a new location, it is highly recommended that you follow these steps and implement the new install point before taking the current ClickOnce install point offline.

  1. In a text editor, create a new file.
  2. On the first line of the file, add the fully qualified location for the new install location. (i.e. Save the new file to http://www.example.com/ClickOnceInstall_NewLocation/)
  3. Save the file as "reinstall" to the root of your current applications ClickOnce install location. (i.e http://www.example.com/ClickOnceInstall/reinstall where http://www.example.com/ClickOnceInstall/ is the root of the installation path.)
  4. Launch your application from your test machine. The application should automatically uninstall your current version of your application and reinstall it from the location specified in the reinstall file.

C. Special Notes

  1. You do not have to save the reinstall file to the root of the original application installation folder, however, you will need to publish an version of your application to the original install point that references a web address that will contain a reinstall file that will specify the new install point.

    This requires a bit of pre-planning so that a reference can be made from the application to a path that you know you will have control of.

  2. The reinstall file can be saved to the root of the initial install location but must be left empty if the application does not yet need to be reinstalled.
    An empty reinstall file is ignored.

  3. Technically, the API looks for a web resonse from a call to "reinstall". A mechanism could potentially be implemented on the server that returns a text reponse with the location of the new installation.

  4. The reinstall file is parsed by looking at the first line of the file for the location of the new installation. All other text is ignored. This is intentional so that subsequent updates to this API can potentially implement newer properties in the reinstall response.

  5. The API in it's current state will only support ClickOnce applications that have been installed under an English culture variant. The reason for this constraint is because the process is automated by looking for the uninstall dialog and passing a Click command to a button that has the text value of "OK".

Solution 3

For the crazy or desperate, reflection to the rescue! Replace the "X"s with your app's .application file name (not path) and public key token.

Tested on Windows 10 only.

        var textualSubId = "XXXXXXXXXXXXXXXXXX.application, Culture=neutral, PublicKeyToken=XXXXXXXXXXXXXXXX, processorArchitecture=amd64";

        var deploymentServiceCom = new System.Deployment.Application.DeploymentServiceCom();
        var _r_m_GetSubscriptionState = typeof(System.Deployment.Application.DeploymentServiceCom).GetMethod("GetSubscriptionState", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

        var subState = _r_m_GetSubscriptionState.Invoke(deploymentServiceCom, new[] { textualSubId });
        var subscriptionStore = subState.GetType().GetProperty("SubscriptionStore").GetValue(subState);
        subscriptionStore.GetType().GetMethod("UninstallSubscription").Invoke(subscriptionStore, new[] { subState });

Hope this helps someone.

Solution 4

Have a look at this thread: http://social.msdn.microsoft.com/Forums/en-US/winformssetup/thread/4b681725-faaa-48c3-bbb0-02ebf3926e25

It gives the link to the following blog, where the code uninstalls the application, and then re-installs the application, you could probably want just to uninstall. have a look at it.

http://www.jamesharte.com/blog/?p=11

Share:
12,379
dwolvin
Author by

dwolvin

Updated on June 05, 2022

Comments

  • dwolvin
    dwolvin almost 2 years

    Can I reliably initiate the un-installation of a ClickOnce app, from within the app?

    In other words, I want to give the user a big "Uninstall Me Now" button on one of the forms. When a user clicks the button, I want to start the windows uninstall process for this app, and perhaps close the app.

    Reason: We are dead-ending a ClickOnce app and want to make it as easy to remove as it was to install. We don't want to send them down the path to "Add or Remove Programs" and risk them getting lost or distracted.

    Can this be done reliably?

  • richard
    richard over 13 years
    If you use the information in that article, here are 2 things to watch out for. 1. Make sure the update for the app that is uninstalling itself is not optional. You need to make the required version be the current deployed version. Otherwise, the restore option is available and is the default for the uninstall dialog box, and the app won't uninstall itself. 2. Make sure in the GetUninstallString method in the DeploymentUtils class, change the DisplayName it is looking for from "TestCertExp_CSharp" to the name of your app.
  • David Murdoch
    David Murdoch about 9 years
    -1 Links to code in SO answers are why we can't have nice things. The .zip with the code in it is not longer available. While, yes, most of the code is in the article and can be copy pasted out, the DeploymentUtilsWin32 class is not.
  • George Tsiokos
    George Tsiokos over 7 years
    +1 Thank you! Using .NET 4.6, my textualSubId had to be in the format XXXXXXXXXXXXXXXXXX.app -> .app not .application
  • Multinerd
    Multinerd about 7 years
    Funny enough, your link is dead. next time paste the code here instead of just linking
  • Gab
    Gab about 2 years
    Thanks. Still works, I used it to rename a ClickOnce application. I had to deploy a new version of the app with the old name, that only show a screen with a button executing this code. Note that appKey.Close(); was called too early, and I had to run IE with "reinstallServerFile" instead of "newAddr"