【问题标题】:How to initiate an uninstall of a ClickOnce app, from within the app?如何从应用程序内启动 ClickOnce 应用程序的卸载?
【发布时间】:2011-02-09 22:19:31
【问题描述】:

我是否可以从应用内可靠地启动 ClickOnce 应用的卸载?

换句话说,我想在其中一个表单上为用户提供一个很大的“立即卸载我”按钮。当用户单击按钮时,我想启动此应用程序的 Windows 卸载过程,并可能关闭该应用程序。

原因:我们正在终止 ClickOnce 应用程序,并希望使其易于删除就像安装一样。我们不想发送它们沿着“添加或删除程序”的路径前进,并冒着迷路或分心的风险。

这可以可靠地完成吗?

【问题讨论】:

    标签: .net winforms deployment clickonce


    【解决方案1】:

    我建议您在此处查看此 MSDN 文章。它解释了如何以编程方式卸载应用程序(如果您愿意,还可以从新 URL 重新安装):

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

    这是 jameshart 博客条目的一个变体,但它包含一些您将要使用的修复程序。 C#和VB都有代码下载。

    事实上,你可以只推送更新,然后让应用自行卸载,甚至不需要用户说“ok”。

    【讨论】:

    • 如果您使用该文章中的信息,请注意以下两点。 1. 确保正在卸载的应用程序的更新不是可选的。您需要将所需的版本设为当前部署的版本。否则,恢复选项可用并且是卸载对话框的默认选项,并且应用程序不会自行卸载。 2. 确保在 DeploymentUtils 类的 GetUninstallString 方法中,将它正在寻找的 DisplayName 从“TestCertExp_CSharp”更改为您的应用程序的名称。
    • -1 SO 答案中的代码链接是我们无法拥有美好事物的原因。包含代码的 .zip 不再可用。虽然,是的,大部分代码都在文章中,可以复制粘贴,但 DeploymentUtilsWin32 类不是。
    【解决方案2】:

    我将这里留给任何来寻找代码并发现其他答案中的下载链接已失效的人:

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

    编辑:添加了 Reinstaller.cs 中的代码和 ReadMe.txt 中的说明

    /* ClickOnceReinstaller v 1.0.0
     *  - Author: Richard Hartness (rhartness@gmail.com)
     *  - 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
        }
    }
    

    ReadMe.txt 中的说明:

    A.在当前应用程序中引用此 API。

    按照这些说明为将来的申请准备申请 从不同的安装点重新安装。这些步骤添加了必要的库 引用,以便您的应用程序可以自动从新位置重新安装。
    即使尚未进行新安装,也可以随时执行这些步骤 必要的。

    1. 打开 ClickOnceReinstaller 项目并在 Release 模式下构建项目。

    2. 打开您的 ClickOnce 应用程序并引用 ClickOnceReinstaller.dll 文件到您的启动项目。

      或者,您可以将 ClickOnceReinstaller 项目添加到您的应用程序 并引用该项目。

    3. 接下来,打开包含应用程序入口点的代码文件。 (通常,在 C# 中,这是 Program.cs)

      从应用程序入口点文件中,调用 Reinstaller.CheckForUpdates() 函数。有几种方法 CheckForUpdates() 的签名。请参阅 Intellisense 描述 确定为您的应用程序调用哪个签名。最初,这 应该无关紧要,因为不应将必要的查找文件发布到 您的安装服务器。

      (可选)Reinstaller.CheckForUpdates 方法返回一个 InstallStatus 对象 这是安装过程状态的枚举值。捕捉这个 重视并相应地处理它。每个潜在返回值的定义 可以通过 Intellisense 找到每个值。

      NoUpdates 响应意味着当前没有新的更新需要 重新安装您的应用程序。

    4. 测试编译您的应用程序并将应用程序的新版本重新发布到 安装服务器。

    B.从新的安装位置更新您的应用程序

    一旦应用程序需要移动到新的网址或 需要对需要重新安装的应用程序进行更改 应用。

    如果您的网络服务器需要移动到新位置,强烈建议您 遵循这些步骤并实施新的安装点,然后再采用当前的安装点 ClickOnce 安装点离线。

    1. 在文本编辑器中,创建一个新文件。
    2. 在文件的第一行,添加新安装的完全限定位置 地点。 (即将新文件保存到http://www.example.com/ClickOnceInstall_NewLocation/
    3. 将文件另存为“重新安装”到当前应用程序 ClickOnce 的根目录 安装位置。 (即http://www.example.com/ClickOnceInstall/reinstall http://www.example.com/ClickOnceInstall/ 是安装路径的根目录。)
    4. 从您的测试机器启动您的应用程序。该应用程序应 自动卸载您当前版本的应用程序并重新安装 它来自重新安装文件中指定的位置。

    C.特别说明

    1. 您不必将重新安装文件保存到原始根目录 应用程序安装文件夹,但是,您需要发布一个版本的 您的应用程序到引用 Web 地址的原始安装点 这将包含一个重新安装文件,该文件将指定新的安装点。

      这需要一些预先计划,以便可以从 应用到您知道可以控制的路径。

    2. 可以将重新安装文件保存到初始安装位置的根目录 但如果还不需要重新安装应用程序,则必须留空。
      空的重新安装文件将被忽略。

    3. 从技术上讲,API 从“重新安装”调用中寻找网络响应。 一种机制可能会在服务器上实现,它返回一个 文本回复新安装的位置。

    4. 通过查看文件的第一行来解析重新安装文件 新安装的位置。所有其他文本都被忽略。这 是有意的,以便此 API 的后续更新可能会实现 重新安装响应中的更新属性。

    5. 当前状态下的 API 将仅支持 ClickOnce 应用程序 已安装在英语文化变体下。这样做的原因 约束是因为该过程是通过查找卸载来自动化的 对话框并将单击命令传递给具有文本值的按钮 “好的”。

    【讨论】:

    • 很有趣,您的链接已失效。下次将代码粘贴到这里而不是仅仅链接
    【解决方案3】:

    看看这个线程: http://social.msdn.microsoft.com/Forums/en-US/winformssetup/thread/4b681725-faaa-48c3-bbb0-02ebf3926e25

    它提供了以下博客的链接,其中代码卸载应用程序,然后重新安装应用程序,您可能只想卸载。看看吧。

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

    【讨论】:

      【解决方案4】:

      对于疯狂或绝望的人,反思来拯救!将“X”替换为您应用的 .application 文件名(不是路径)和公钥令牌。

      仅在 Windows 10 上测试。

              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 });
      

      希望这对某人有所帮助。

      【讨论】:

      • +1 谢谢!使用 .NET 4.6,我的 textualSubId 的格式必须为 XXXXXXXXXXXXXXXXXX.app -> .app 而不是 .application
      猜你喜欢
      • 2017-09-06
      • 2012-02-03
      • 2011-06-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多