静默可更新的单实例 WPF ClickOnce 应用程序






4.95/5 (28投票s)
可静默更新的单实例WPF ClickOnce应用程序。
- 下载源代码 - 17.2 KB
- GitHub上的源代码。 请参阅下面的“如何使用示例应用程序”部分。
简介
ClickOnce提供了一个API,可以轻松控制和自定义简单的更新流程。我需要为我的应用程序添加强制更新功能,并尽量减少用户交互。通过这段代码,您可以完全无需用户干预,或者通知用户并提供,例如,一个重启按钮。此应用程序的更新在后台运行,并在完成后提供一个事件和属性。该应用程序是单实例的,并具有重启功能,这在ClickOnce部署中可能不容易实现。
使用代码
代码量不多。所有讨论的功能都在SilentUpdater
和SingleInstanceApplication
类中。
使用SilentUpdater更新
此ClickOnce更新实现会定期检查更新,如果存在任何更新(必需或非必需),它会触发一个事件,还有一个属性指示更新是否可用(UpdateAvailabe
)。此功能在SilentUpdater
类中。它还实现了INotifyPropertyChanged
,因此为UpdateAvailabe
属性提供了PropertyChanged
事件。
为什么是静默的?好吧,使用SilentUpdater
,您可以在不向用户显示所有这些标准的ClickOnce对话框的情况下更新和重启应用程序。当有可用更新时,您可以静默重启应用程序(SingleInstanceApplication
有一个Restart
方法),或者为用户提供一个重启按钮/通知。在用户手动重启应用程序时,它也会被更新。所以这是一种强制应用程序更新的方式。
public class SilentUpdater : INotifyPropertyChanged
{
private readonly ApplicationDeployment applicationDeployment;
private readonly Timer timer = new Timer(60000);
private bool processing;
public event EventHandler<UpdateProgressChangedEventArgs> ProgressChanged;
public event EventHandler<EventArgs> Completed;
public event PropertyChangedEventHandler PropertyChanged;
private bool updateAvailable;
public bool UpdateAvailable
{
get { return updateAvailable; }
private set { updateAvailable = value; OnPropertyChanged("UpdateAvailable"); }
}
protected virtual void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
private void OnCompleted()
{
var handler = Completed;
if (handler != null) handler(this, null);
}
private void OnProgressChanged(UpdateProgressChangedEventArgs e)
{
var handler = ProgressChanged;
if (handler != null) handler(this, e);
}
public SilentUpdater()
{
if (!ApplicationDeployment.IsNetworkDeployed)
return;
applicationDeployment = ApplicationDeployment.CurrentDeployment;
applicationDeployment.UpdateCompleted += UpdateCompleted;
applicationDeployment.UpdateProgressChanged += UpdateProgressChanged;
timer.Elapsed += (sender, args) =>
{
if (processing)
return;
processing = true;
try
{
if (applicationDeployment.CheckForUpdate(false))
applicationDeployment.UpdateAsync();
else
processing = false;
}
catch(Exception ex)
{
Debug.Write("Check for update failed. " + ex.Message);
processing = false;
}
};
timer.Start();
}
void UpdateProgressChanged(object sender, DeploymentProgressChangedEventArgs e)
{
OnProgressChanged(new UpdateProgressChangedEventArgs(e));
}
void UpdateCompleted(object sender, AsyncCompletedEventArgs e)
{
processing = false;
if (e.Cancelled || e.Error != null)
{
Debug.WriteLine("Could not install the latest version of the application.");
return;
}
UpdateAvailable = true;
OnCompleted();
}
}
以前版本的SilentUpdater
使用CheckForUpdateAsync
,但 如果您的代码检查了更新但未应用,然后重启,这可能会导致更新对话框窗口出现。现在SilentUpdater
调用applicationDeployment.CheckForUpdate(persistUpdateCheckResult:false)
,这不会持久化更新可用状态,也不会显示更新对话框窗口。
MSDN说:
如果CheckForUpdate
发现有可用更新,并且用户选择不安装它,ClickOnce将在下次运行应用程序时提示用户有可用更新。没有办法禁用此提示。(如果应用程序是必需的更新,ClickOnce将自动安装而无需提示。)
另外,只要我使用System.Timers.Timer
,Elapsed
事件处理程序就会在另一个线程上启动,因此可以安全地使用CheckForUpdate
方法而不会冻结。
您经常会看到这样的代码:
if (!ApplicationDeployment.IsNetworkDeployed)
return;
这意味着您不能依赖CurrentDeployment
会被初始化的事实。但是,如果您的应用程序只使用ClickOnce部署并且这不会改变,那么这个检查是多余的。在这种情况下,您可能会遇到异常,但这将意味着您的应用程序部署出现了绝对错误的问题。微软文档说:
CurrentDeployment
静态属性仅在ClickOnce部署的应用程序内部有效。尝试从非ClickOnce应用程序调用此属性将引发异常。如果您正在开发一个可能被ClickOnce部署也可能不被ClickOnce部署的应用程序,请使用IsNetworkDeployed
属性来测试当前程序是否为ClickOnce应用程序。
但是有时这可能会导致调试失败,所以我会在我的代码中使用它。
SingleInstanceApplication中的单实例和重启
该应用程序是单实例的。为此目的使用了Mutex。
bool createdNew;
instanceMutex = new Mutex(true, @"Local\" + Assembly.GetExecutingAssembly().GetType().GUID, out createdNew);
if (!createdNew)
{
instanceMutex = null;
Current.Shutdown();
return;
}
逻辑很简单 - 如果当前用户的此应用程序的互斥体已创建,则停止执行。使用“Local\"前缀,会创建当前用户的互斥体,使用“Global\"将互斥体的范围设置为当前机器。
您不应忘记在退出或重启时释放和关闭互斥体:
private void ReleaseMutex()
{
if (instanceMutex == null)
return;
instanceMutex.ReleaseMutex();
instanceMutex.Close();
instanceMutex = null;
}
Restart方法如下:
ReleaseMutex();
proc.Start();
Current.Shutdown();
首先应释放互斥体,然后启动应用程序的新实例,当前实例将关闭。
重启已更新的ClickOnce应用程序可能会遇到问题,因为如果您重启一个可执行文件,旧版本将被启动。有时人们会引用System.Windows.Forms.dll并调用
System.Windows.Forms.Application.Restart();
但这对于实现所需功能来说太复杂了。您可以直接启动appref-ms文件。
您可以在桌面/开始菜单中搜索它,或者生成临时的appref-ms文件。 在下面的代码中,您可以找到这两种选项。在注释掉的部分中,有一个调用CreateClickOnceShortcut
的调用,它将生成一个临时的appref-ms文件。Restart
方法中的第一个未注释行调用GetShortcutPath
,它返回安装时生成的appref-ms文件的路径:
public void Restart()
{
//var shortcutFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".appref-ms");
//CreateClickOnceShortcut(tmpFile);
var shortcutFile = GetShortcutPath();
var proc = new Process { StartInfo = { FileName = shortcutFile, UseShellExecute = true } };
ReleaseMutex();
proc.Start();
Current.Shutdown();
}
public static string GetShortcutPath()
{
return String.Format(@"{0}\{1}\{2}.appref-ms", Environment.GetFolderPath(Environment.SpecialFolder.Programs), GetPublisher(), GetDeploymentInfo().Name.Replace(".application", ""));
}
public static string GetPublisher()
{
XDocument xDocument;
using (var memoryStream = new MemoryStream(AppDomain.CurrentDomain.ActivationContext.DeploymentManifestBytes))
using (var xmlTextReader = new XmlTextReader(memoryStream))
xDocument = XDocument.Load(xmlTextReader);
if (xDocument.Root == null)
return null;
var description = xDocument.Root.Elements().First(e => e.Name.LocalName == "description");
var publisher = description.Attributes().First(a => a.Name.LocalName == "publisher");
return publisher.Value;
}
private static ApplicationId GetDeploymentInfo()
{
var appSecurityInfo = new System.Security.Policy.ApplicationSecurityInfo(AppDomain.CurrentDomain.ActivationContext);
return appSecurityInfo.DeploymentId;
}
private static void CreateClickOnceShortcut(string location)
{
var updateLocation = System.Deployment.Application.ApplicationDeployment.CurrentDeployment.UpdateLocation;
var deploymentInfo = GetDeploymentInfo();
using (var shortcutFile = new StreamWriter(location, false, Encoding.Unicode))
{
shortcutFile.Write(String.Format(@"{0}#{1}, Culture=neutral, PublicKeyToken=",
updateLocation.ToString().Replace(" ", "%20"),
deploymentInfo.Name.Replace(" ", "%20")));
foreach (var b in deploymentInfo.PublicKeyToken)
shortcutFile.Write("{0:x2}", b);
shortcutFile.Write(String.Format(", processorArchitecture={0}", deploymentInfo.ProcessorArchitecture));
shortcutFile.Close();
}
} <span style="font-size: 9pt;"> </span>
如何使用示例应用程序
更改tmpFileContent(使用appref-ms获取所需内容)不相关,因为我已删除硬编码的变量- 将其发布到某个位置
(确保tmpFileContent
与您设置的位置匹配)。 - 从此位置安装应用程序。
- 再次发布到同一位置。
- 等待一分钟,观察UI已更新并且出现重启按钮。
- 按Restart按钮 -> 启动了一个新应用程序。
历史
- 2013.04.21 - 移除了appref-ms文件的硬编码内容,现在您可以动态生成此链接或使用默认的生成路径。
- 2012.11.21 - 更新。 在以前的版本中,如果执行了更新检查但更新因某种原因未能完成,在下次启动应用程序时,会显示一个默认对话框。现在此状态不会被持久化,因此不会显示对话框窗口。
- 2012.11.06 - 添加了下载链接。
- 2012.11.06 - 初始版本。