65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (28投票s)

2012年11月6日

CPOL

4分钟阅读

viewsIcon

65853

downloadIcon

1331

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

简介

ClickOnce提供了一个API,可以轻松控制和自定义简单的更新流程。我需要为我的应用程序添加强制更新功能,并尽量减少用户交互。通过这段代码,您可以完全无需用户干预,或者通知用户并提供,例如,一个重启按钮。此应用程序的更新在后台运行,并在完成后提供一个事件和属性。该应用程序是单实例的,并具有重启功能,这在ClickOnce部署中可能不容易实现。 

使用代码

代码量不多。所有讨论的功能都在SilentUpdaterSingleInstanceApplication类中。 

使用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.TimerElapsed事件处理程序就会在另一个线程上启动,因此可以安全地使用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按钮 -> 启动了一个新应用程序。 
 当应用更新并需要重启时,示例应用程序将显示更新后的UI。 

 

历史 

  • 2013.04.21 - 移除了appref-ms文件的硬编码内容,现在您可以动态生成此链接或使用默认的生成路径。 
  • 2012.11.21 - 更新。 在以前的版本中,如果执行了更新检查但更新因某种原因未能完成,在下次启动应用程序时,会显示一个默认对话框。现在此状态不会被持久化,因此不会显示对话框窗口。  
  • 2012.11.06 - 添加了下载链接。   
  • 2012.11.06 - 初始版本。
© . All rights reserved.