ClickOnce 应用程序自动启动和干净卸载, 或自定义 ClickOnce 安装的方法






4.88/5 (24投票s)
实现 ClickOnce 的轻量级安装/卸载自定义的方法,包括自动启动和卸载时清理。
引言
ClickOnce 是一项不错的技术,但在某些情况下很难自定义。最近我有一个需求,要求 ClickOnce 应用程序在启动时运行。听起来很简单,对吧?但对于 ClickOnce 来说并非如此。你可以通过搜索发现,这曾是许多开发者的一个实际问题。 甚至 MSDN 在比较图表(MSI vs. ClickOnce)中也表示,ClickOnce 部署不支持启动时运行,并且在注册表访问方面也有局限性。
在本文中,我将介绍解决此问题的一种方法。它可能存在一些缺点,但很可靠,并且可以用于某些应用程序。
功能
此解决方案允许您:
- 在启动组中添加应用程序的快捷方式,以便在启动时运行
- 在卸载时从启动组删除快捷方式
- 在卸载时清理任何您想清理的内容
- 卸载时自动关闭应用程序(实际上是终止进程,但您可以添加一些消息提示并要求您的程序关闭)
- 在应用程序向导中设置您喜欢的图标,而不是默认图标
- 在应用程序向导中为您的应用程序设置一些帮助和关于链接
使用代码
注意:首次,请至少一次以 Release 模式构建解决方案,主项目中有一个链接(Release 文件夹中应存在 "uninstall.exe")。
该解决方案包含三个项目
- WPF 应用程序,即我们要分发的应用程序
- 公共库,包含
ClickOnceHelper
- 卸载应用程序,在卸载时启动
WPF 应用程序和卸载应用程序都引用公共库。WPF 应用程序是要发布的应用程序,因此我们需要它包含实际的卸载可执行文件。为此,WPF 项目依赖于卸载项目,以便在主应用程序构建之前出现 *uninstall.exe*。为了能够将 *uninstall.exe* 包含在部署中,我从 *Release* 目录添加了一个指向 *uninstall.exe* 的链接。(我当时是在 Release 模式下构建的 *uninstall.exe*:右键单击主项目 -> 添加现有项 -> 在 *release* 目录中找到 *uninstall.exe* -> 从“添加”按钮的下拉菜单中选择“添加链接”)。因此,如果 Release 目录中没有 *uninstall.exe*,主项目将无法构建。请构建一次 Release 模式以进行调试/编译。
主应用程序
值得关注的代码位于 App 类中
protected override void OnStartup(StartupEventArgs e)
{
try
{
var clickOnceHelper = new ClickOnceHelper(Globals.PublisherName, Globals.ProductName);
clickOnceHelper.UpdateUninstallParameters();
clickOnceHelper.AddShortcutToStartup();
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
base.OnStartup(e);
}
每次启动时,都会检查应用程序和快捷方式的注册表参数。如果是第一次运行或有更新,注册表值将被更新,如果图标被删除,将重新创建。如果一切正常,代码几乎不做任何事情。
卸载应用程序
这是一个简单的应用程序。它有一个互斥锁来检查单实例,并有代码执行卸载。
代码显示一个 Y/N 对话框。如果用户选择卸载应用程序,代码将使用 ClickOnceHelper
自动执行 ClickOnce 卸载,并执行您希望执行的其他自定义操作。在这种情况下,将从 AppData 中删除应用程序数据文件夹,该文件夹本应由主应用程序使用。
if (MessageBox.Show(Resources.Uninstall_Question, Resources.Uninstall +
Globals.ProductName, MessageBoxButtons.YesNo) == DialogResult.Yes)
{
var clickOnceHelper = new ClickOnceHelper(Globals.PublisherName, Globals.ProductName);
clickOnceHelper.Uninstall();
//Delete all files from publisher folder and folder itself on uninstall
var publisherFolder = Path.Combine(Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData), Globals.PublisherName);
if (Directory.Exists(publisherFolder))
Directory.Delete(publisherFolder, true);
}
此外,命名为 setup*、install*、uninstall* 的 Windows 兼容性文件可能会导致应用程序崩溃,这是因为 Windows 不确定这些“setup”是否已正确完成。为了避免这种可能的应用程序崩溃,请嵌入自定义清单并取消注释支持的 OS GUID。
ClickOnceHelper
功能的核心在这个类中。我将其分为三个区域
- 快捷方式相关
- 更新注册表
- Uninstall
要使用此类,您需要提供发布者名称和产品名称。**它们应与主项目中的“发布”->“选项”值匹配。** 您可以在配置文件中提供它们,硬编码,或在运行时确定。在这些解决方案中,它们已在静态 Clobals
类中硬编码。
创建时,它会在应用程序数据中创建一个目录来存储原始卸载字符串:
var publisherFolder = Path.Combine(Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData), PublisherName);
if (!Directory.Exists(publisherFolder))
Directory.CreateDirectory(publisherFolder);
UninstallFile = Path.Combine(publisherFolder, UninstallStringFile);
它还在注册表中查找应用程序的卸载部分(注册表项):
UninstallRegistryKey = GetUninstallRegistryKeyByProductName(ProductName);
快捷方式代码:
private string GetShortcutPath()
{
var allProgramsPath = Environment.GetFolderPath(Environment.SpecialFolder.Programs);
var shortcutPath = Path.Combine(allProgramsPath, PublisherName);
return Path.Combine(shortcutPath, ProductName) + ApprefExtension;
}
private string GetStartupShortcutPath()
{
var startupPath = Environment.GetFolderPath(Environment.SpecialFolder.Startup);
return Path.Combine(startupPath, ProductName) + ApprefExtension;
}
public void AddShortcutToStartup()
{
if (!ApplicationDeployment.IsNetworkDeployed)
return;
var startupPath = GetStartupShortcutPath();
if (File.Exists(startupPath))
return;
File.Copy(GetShortcutPath(), startupPath);
}
private void RemoveShortcutFromStartup()
{
var startupPath = GetStartupShortcutPath();
if (File.Exists(startupPath))
File.Delete(startupPath);
}
此代码仅将安装后程序中的现有快捷方式复制到启动组。RemoveShortcutFromStartup
在卸载时被调用。
您还可以通过添加(并在卸载时删除)另一个注册表项来让您的应用程序在启动时运行
// The path to the key where Windows looks for startup applications
RegistryKey rkApp = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", true);
//Path to launch shortcut
string startPath = Environment.GetFolderPath(Environment.SpecialFolder.Programs) +
@"\YourPublisher\YourSuite\YourProduct.appref-ms";
rkApp.SetValue("YourProduct", startPath);
但在本文中,我们只是从应用程序启动组复制/删除图标。
注册表更新
此处设置注册表中的参数,这些参数允许在应用程序向导中设置自定义图标(DisplayIcon 键)、卸载字符串(UninstallString 键)等。卸载字符串设置为 "uninstall.exe",原始值存储在 AppData 文件夹中的 UninstallString.bat 文件中。
public void UpdateUninstallParameters()
{
if (UninstallRegistryKey == null)
return;
UpdateUninstallString();
UpdateDisplayIcon();
SetNoModify();
SetNoRepair();
SetHelpLink();
SetURLInfoAbout();
}
private RegistryKey GetUninstallRegistryKeyByProductName(string productName)
{
var subKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall");
if (subKey == null)
return null;
foreach (var name in subKey.GetSubKeyNames())
{
var application = subKey.OpenSubKey(name, RegistryKeyPermissionCheck.ReadWriteSubTree,
RegistryRights.QueryValues | RegistryRights.ReadKey | RegistryRights.SetValue);
if (application == null)
continue;
foreach (var appKey in application.GetValueNames().Where(appKey => appKey.Equals(DisplayNameKey)))
{
if (application.GetValue(appKey).Equals(productName))
return application;
break;
}
}
return null;
}
private void UpdateUninstallString()
{
var uninstallString = (string)UninstallRegistryKey.GetValue(UninstallString);
if (!String.IsNullOrEmpty(UninstallFile) && uninstallString.StartsWith("rundll32.exe"))
File.WriteAllText(UninstallFile, uninstallString);
var str = String.Format("\"{0}\" uninstall",
Path.Combine(Path.GetDirectoryName(Location), "uninstall.exe"));
UninstallRegistryKey.SetValue(UninstallString, str);
}
private void UpdateDisplayIcon()
{
var str = String.Format("{0},0",
Path.Combine(Path.GetDirectoryName(Location), "uninstall.exe"));
UninstallRegistryKey.SetValue("DisplayIcon", str);
}
private void SetNoModify()
{
UninstallRegistryKey.SetValue("NoModify", 1, RegistryValueKind.DWord);
}
private void SetNoRepair()
{
UninstallRegistryKey.SetValue("NoRepair", 1, RegistryValueKind.DWord);
}
private void SetHelpLink()
{
UninstallRegistryKey.SetValue("HelpLink", Globals.HelpLink, RegistryValueKind.String);
}
private void SetURLInfoAbout()
{
UninstallRegistryKey.SetValue("URLInfoAbout", Globals.AboutLink, RegistryValueKind.String);
}
ClickOnceHelper 卸载
在 ClickOnceHelper 卸载方法执行期间,首先会终止应用程序进程(您可以在此处添加一些消息提示,要求您的应用程序关闭而不是终止进程,在实际应用中您不应该终止它)。
然后删除启动快捷方式,最后自动化标准的 ClickOnce 卸载。
ClickOnce 默认在卸载时显示“恢复/删除”对话框。在此场景中,我不想提供这些选项或允许用户与此对话框交互。因此,会启动一个新进程,其中包含来自 *UninstallString.bat* 的数据。在卸载期间,会显示一个自定义对话框,因此在这里我们只是自动化标准对话框的“删除”流程。
我之前看到过对该对话框的自动化,但它们有点错误
- 曾使用此类代码
SendKeys.SendWait("+{TAB}"); // SHIFT-TAB
。但这需要窗口处于前台并获得焦点,因此用户或系统很容易导致此自动化失败。 - 该流程仅涵盖了一种情况,即禁用“恢复”。但是,如果“恢复”可用,它会获得焦点,您需要将其切换为“删除”!我使用的是正确的流程:Shift+Tab,Shift+Tab,Down,Tab,Enter。
public void Uninstall()
{
try
{
//kill process
foreach (var process in Process.GetProcessesByName(ProductName))
{
process.Kill();
break;
}
if (!File.Exists(UninstallFile))
return;
RemoveShortcutFromStartup();
var uninstallString = File.ReadAllText(UninstallFile);
var fileName = uninstallString.Substring(0, uninstallString.IndexOf(" "));
var args = uninstallString.Substring(uninstallString.IndexOf(" ") + 1);
var proc = new Process
{
StartInfo =
{
Arguments = args,
FileName = fileName,
UseShellExecute = false
}
};
proc.Start();
RespondToClickOnceRemovalDialog();
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true)]
static extern bool PostMessage(IntPtr hWnd,
[MarshalAs(UnmanagedType.U4)] uint Msg, IntPtr wParam, IntPtr lParam);
const int WM_KEYDOWN = 0x0100;
const int WM_KEYUP = 0x0101;
private void RespondToClickOnceRemovalDialog()
{
var myWindowHandle = IntPtr.Zero;
for (var i = 0; i < 250 && myWindowHandle == IntPtr.Zero; i++)
{
Thread.Sleep(150);
foreach (var proc in Process.GetProcessesByName("dfsvc"))
if (!String.IsNullOrEmpty(proc.MainWindowTitle) &&
proc.MainWindowTitle.StartsWith(ProductName))
{
myWindowHandle = proc.MainWindowHandle;
break;
}
}
if (myWindowHandle == IntPtr.Zero)
return;
SetForegroundWindow(myWindowHandle);
Thread.Sleep(100);
const uint wparam = 0 << 29 | 0;
PostMessage(myWindowHandle, WM_KEYDOWN, (IntPtr)(Keys.Shift | Keys.Tab), (IntPtr)wparam);
PostMessage(myWindowHandle, WM_KEYDOWN, (IntPtr)(Keys.Shift | Keys.Tab), (IntPtr)wparam);
PostMessage(myWindowHandle, WM_KEYDOWN, (IntPtr)Keys.Down, (IntPtr)wparam);
PostMessage(myWindowHandle, WM_KEYDOWN, (IntPtr)Keys.Tab, (IntPtr)wparam);
PostMessage(myWindowHandle, WM_KEYDOWN, (IntPtr)Keys.Enter, (IntPtr)wparam);
}
历史
- 2012.12.11 - 一些修正。添加了通过仅更新注册表启动部分来实现启动时运行的注释。
- 2012.12.10 - 初始版本。