如何從應用程序內啟動 ClickOnce 應用程序的解除安裝?
我可以從應用程序內可靠地啟動 ClickOnce 應用程序的解除安裝嗎?
換句話說,我想在其中一個表單上為使用者提供一個很大的“立即解除安裝我”按鈕。當使用者點擊按鈕時,我想啟動此應用程序的 Windows 解除安裝過程,並可能關閉該應用程序。
原因:我們正在終結 ClickOnce 應用程序,並希望讓它像***安裝一樣容易刪除。***我們不想讓他們走上“添加或刪除程序”的道路,並冒著讓他們迷路或分心的風險。
這可以可靠地完成嗎?
我建議在這裡查看這篇 MSDN 文章。它解釋瞭如何以程式方式解除安裝應用程序(如果需要,還可以從新 URL 重新安裝):
http://msdn.microsoft.com/en-us/library/ff369721.aspx
這是 jameshart 部落格條目的一個變體,但它包含一些您將要使用的修復程序。C# 和 VB 都有程式碼下載。
實際上,您可以只推送更新並讓應用自行解除安裝,甚至不需要使用者說“ok”。
我將把這個留給任何來尋找程式碼的人,並發現其他答案中的下載連結已經失效:
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。
按照這些說明準備您的應用程序,以便將來從不同的安裝點重新安裝應用程序。這些步驟添加了必要的庫引用,以便您的應用程序可以自動從新位置重新安裝。
任何時候都可以執行這些步驟,即使還沒有必要進行新安裝。
- 打開 ClickOnceReinstaller 項目並在發布模式下建構項目。
- 打開 ClickOnce 應用程序和對 ClickOnceReinstaller.dll 文件的引用到您的啟動項目。
或者,您可以將 ClickOnceReinstaller 項目添加到您的應用程序並引用該項目。 3. 接下來,打開包含應用程序入口點的程式碼文件。(通常,在 C# 中,這是 Program.cs)
從應用程序入口點文件中,呼叫 Reinstaller.CheckForUpdates() 函式。CheckForUpdates() 有幾個方法簽名。請參閱 Intellisense 描述以確定為您的應用程序呼叫哪個簽名。最初,這應該無關緊要,因為不應將必要的查找文件發佈到您的安裝伺服器。
(可選)Reinstaller.CheckForUpdates 方法返回一個 InstallStatus 對象,該對像是安裝過程狀態的列舉值。擷取這個值並相應地處理它。可以通過 Intellisense 找到每個潛在返回值的定義。
NoUpdates 響應意味著目前沒有新的更新需要重新安裝您的應用程序。 4. 測試編譯您的應用程序並將應用程序的新版本重新發佈到安裝伺服器。
B. 從新的安裝位置更新您的應用程序
一旦應用程序需要移動到新的網址或需要對需要重新安裝應用程序的應用程序進行更改,就需要執行這些步驟。
如果您的 Web 伺服器需要移動到新位置,強烈建議您在使目前 ClickOnce 安裝點離線之前執行這些步驟並實施新安裝點。
- 在文本編輯器中,創建一個新文件。
- 在文件的第一行,添加新安裝位置的完全限定位置。(即將新文件保存到http://www.example.com/ClickOnceInstall_NewLocation/)
- 將文件另存為“重新安裝”到目前應用程序 ClickOnce 安裝位置的根目錄。(即http://www.example.com/ClickOnceInstall/reinstall其中 http://www.example.com/ClickOnceInstall/是安裝路徑的根目錄。)
- 從您的測試機器啟動您的應用程序。該應用程序應自動解除安裝您目前版本的應用程序並從重新安裝文件中指定的位置重新安裝它。
C. 特別說明
- 您不必將重新安裝文件保存到原始應用程序安裝文件夾的根目錄,但是,您需要將應用程序的版本發佈到原始安裝點,該版本引用將包含重新安裝文件的網址,該網址將指定新的安裝點。
這需要一些預先計劃,以便可以從應用程序引用您知道可以控制的路徑。 2. 重新安裝文件可以保存到初始安裝位置的根目錄,但如果還不需要重新安裝應用程序,則必須將其留空。
空的重新安裝文件將被忽略。 3. 從技術上講,API 從“重新安裝”的呼叫中尋找網路響應。可能會在伺服器上實現一種機制,該機制返回帶有新安裝位置的文本響應。 4. 重新安裝文件通過查看文件的第一行來解析新安裝的位置。所有其他文本都被忽略。這是有意的,以便對該 API 的後續更新可能會在重新安裝響應中實現更新的屬性。 5. 目前狀態下的 API 將僅支持在英語文化變體下安裝的 ClickOnce 應用程序。此約束的原因是因為該過程是通過查找解除安裝對話框並將點擊命令傳遞給具有“確定”文本值的按鈕來自動化的。