WPF开发一款软件自动升级组件

软件发布后,自动升级往往是一项必备的功能,本篇博客的目标就是用WPF打造一个自动升级组件。先看效果:升级提醒界面升级过程界面升级完成界面升级界面不是本文的内容

软件发布后,自动升级往往是一项必备的功能,本篇博客的目标就是用WPF打造一个自动升级组件。先看效果:

   升级提醒界面

  

  升级过程界面

  升级完成界面

  升级界面不是本文的内容,具体见我的上一篇博客

 

  其实升级的过程很简单,大致如下:

 

  检测服务器上的版本号—>比较本地程序的版本号和服务器上的版本号—>如果不相同则下载升级的压缩包—>下载完成后解压升级包—>解压后的文件覆盖到应用程序文件目录—>升级完成

 

  有两点需要注意:

 

  因为升级的过程就是用新文件覆盖旧文件的过程,所以要防止老文件被占用后无法覆盖的情况,因而升级之前应该关闭运用程序。

 

  升级程序本身也可能需要升级,而升级程序启动后如问题1所说,就不可能被覆盖了,因而应该想办法避免这种情况。

 

  有了上面的分析,下面我们就来具体实现之。

 

  首先新建一个WPF Application项目,命名为AutoUpdater,因为升级程序需要能够单独执行,必须编译成exe文件,所以不能是类库项目。

 

  接下来新建一个类Updater.cs来处理检测的过程。

 

服务器上的版本信息我们存储到一个XML文件中,文件格式定义如下:

<?xml version="1.0" encoding="utf-8"?>

<UpdateInfo>

    <AppName>Test</AppName>

    <AppVersion>1.0.0.1</AppVersion>

    <RequiredMinVersion>1.0.0.0</RequiredMinVersion>

    <Desc>shengji</Desc>

</UpdateInfo>

 

  然后定义一个实体类对应XML定义的升级信息,如下:

 

public class UpdateInfo

    {

    public string AppName { get; set; }

 

         /// <summary>

         /// 应用程序版本

         /// </summary>

         public Version AppVersion { get; set; }

 

         /// <summary>

         /// 升级需要的最低版本

         /// </summary>

         public Version RequiredMinVersion { get; set; }

 

         public Guid MD5

         {

             get;

              set;

         }

 

        private string _desc;

        /// <summary>

        /// 更新描述

        /// </summary>

        public string Desc

        {

             get

             {

                 return _desc;

             }

             set

             {

                 _desc = string.Join(Environment.NewLine, value.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries));

             }

        }

    }

 

 

 

 

 

  检测的详细步骤应该如下:

 

  异步下载update.xml到本地

 

  分析xml文件信息,存储到自定义的类UpdateInfo中

 

  判断升级需要的最低版本号,如果满足,启动升级程序。这里就碰到了上面提到的问题,文件被占用的问题。因为如果直接启动AutoUpdater.exe,升级包中的AutoUpdater.exe是无法覆盖这个文件的,所以采取的办法是将AutoUpdater.exe拷贝到缓存文件夹中,然后启动缓存文件夹中的AutoUpdater.exe文件来完成升级的过程。

 

  具体代码如下,第一个方法CheckUpdateStatus()完成1、2两个步骤,第二个方法StartUpdate(UpdateInfo updateInfo)完成步骤3:

 

        public static void CheckUpdateStatus()

        {

            System.Threading.ThreadPool.QueueUserWorkItem((s) =>

            {

                string url = Constants.RemoteUrl + Updater.Instance.CallExeName + "/update.xml";

                var client = new System.Net.WebClient();

                client.DownloadDataCompleted += (x, y) =>

                {

                    try

                    {

                        MemoryStream stream = new MemoryStream(y.Result);

 

                        XDocument xDoc = XDocument.Load(stream);

                        UpdateInfo updateInfo = new UpdateInfo();

                        XElement root = xDoc.Element("UpdateInfo");

                        updateInfo.AppName = root.Element("AppName").Value;

                        updateInfo.AppVersion = root.Element("AppVersion") == null || string.IsNullOrEmpty(root.Element("AppVersion").Value) ? null : new Version(root.Element("AppVersion").Value);

                        updateInfo.RequiredMinVersion = root.Element("RequiredMinVersion") == null || string.IsNullOrEmpty(root.Element("RequiredMinVersion").Value) ? null : new Version(root.Element("RequiredMinVersion").Value);

                        updateInfo.Desc = root.Element("Desc").Value;

                        updateInfo.MD5 = Guid.NewGuid();

 

                        stream.Close();

                        Updater.Instance.StartUpdate(updateInfo);

                    }

                    catch

                    { }

                };

                client.DownloadDataAsync(new Uri(url));

 

            });

 

        }

 

        public void StartUpdate(UpdateInfo updateInfo)

        {

            if (updateInfo.RequiredMinVersion != null && Updater.Instance.CurrentVersion < updateInfo.RequiredMinVersion)

            {

                //当前版本比需要的版本小,不更新

                return;

            }

 

            if (Updater.Instance.CurrentVersion >= updateInfo.AppVersion)

            {

                //当前版本是最新的,不更新

                return;

            }

 

            //更新程序复制到缓存文件夹

            string appDir = System.IO.Path.Combine(System.Reflection.Assembly.GetEntryAssembly().Location.Substring(0, System.Reflection.Assembly.GetEntryAssembly().Location.LastIndexOf(System.IO.Path.DirectorySeparatorChar)));

            string updateFileDir = System.IO.Path.Combine(System.IO.Path.Combine(appDir.Substring(0, appDir.LastIndexOf(System.IO.Path.DirectorySeparatorChar))), "Update");

            if (!Directory.Exists(updateFileDir))

            {

                Directory.CreateDirectory(updateFileDir);

            }

            updateFileDir = System.IO.Path.Combine(updateFileDir, updateInfo.MD5.ToString());

            if (!Directory.Exists(updateFileDir))

            {

                Directory.CreateDirectory(updateFileDir);

            }

 

            string exePath = System.IO.Path.Combine(updateFileDir, "AutoUpdater.exe");

            File.Copy(System.IO.Path.Combine(appDir, "AutoUpdater.exe"), exePath, true);

 

            var info = new System.Diagnostics.ProcessStartInfo(exePath);

            info.UseShellExecute = true;

            info.WorkingDirectory = exePath.Substring(0, exePath.LastIndexOf(System.IO.Path.DirectorySeparatorChar));

            updateInfo.Desc = updateInfo.Desc;

            info.Arguments = "update " + Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(CallExeName)) + " " + Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(updateFileDir)) + " " + Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(appDir)) + " " + Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(updateInfo.AppName)) + " " + Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(updateInfo.AppVersion.ToString())) + " " + (string.IsNullOrEmpty(updateInfo.Desc) ? "" : Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(updateInfo.Desc)));

            System.Diagnostics.Process.Start(info);

        }

 

  在方法StartUpdate的最后,启动Autoupdate.exe的代码中,需要将升级信息当做参数传递过去,各参数间用空格分隔,考虑到信息本身(如AppName或Desc中)可能含有空格,所以传递前将信息进行Base64编码。

 

接下来打开Program.cs文件(没有可以自己创建一个,然后在项目属性中修改启动对象,如下图),

  

 

 

 

 

  在Main函数中接收传递过来的参数。代码如下:

 

static void Main(string[] args)
      {
           
if (args.Length == 0)
            {
               
return;
            }
           
else if (args[0] == "update")
            {
               
try
                {
                   
string callExeName = args[1];
                   
string updateFileDir = args[2];
                   
string appDir = args[3];
                   
string appName = args[4];
                   
string appVersion = args[5];
                   
string desc = args[6];

                    Ezhu.AutoUpdater.App app
= new Ezhu.AutoUpdater.App();
                    UI.DownFileProcess downUI
= new UI.DownFileProcess(callExeName, updateFileDir, appDir, appName, appVersion, desc) { WindowStartupLocation = WindowStartupLocation.CenterScreen };
                    app.Run(downUI);
                }
               
catch (Exception ex)
                {
                    MessageBox.Show(ex.Message);
                }
      }

  参数接收成功后,打开下载界面,显示升级的主要内容,如果用户点击升级按钮,则开始下载升级包。

  步骤应该如下:

  关闭应用程序进程

  下载升级包到缓存文件夹

  解压升级包到缓存文件夹

  从缓存文件夹复制解压后的文件和文件夹到运用程序目录

  提醒用户升级成功

  具体代码如下:

public partial class DownFileProcess : WindowBase

    {

        private string updateFileDir;//更新文件存放的文件夹

        private string callExeName;

        private string appDir;

        private string appName;

        private string appVersion;

        private string desc;

        public DownFileProcess(string callExeName, string updateFileDir, string appDir, string appName, string appVersion, string desc)

        {

            InitializeComponent();

            this.Loaded += (sl, el) =>

            {

                YesButton.Content = "现在更新";

                NoButton.Content = "暂不更新";

 

                this.YesButton.Click += (sender, e) =>

                {

                    Process[] processes = Process.GetProcessesByName(this.callExeName);

 

                    if (processes.Length > 0)

                    {

                        foreach (var p in processes)

                        {

                            p.Kill();

                        }

                    }

 

                    DownloadUpdateFile();

                };

 

                this.NoButton.Click += (sender, e) =>

                {

                    this.Close();

                };

 

                this.txtProcess.Text = this.appName + "发现新的版本(" + this.appVersion + "),是否现在更新?";

                txtDes.Text = this.desc;

            };

            this.callExeName = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(callExeName));

            this.updateFileDir = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(updateFileDir));

            this.appDir = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(appDir));

            this.appName = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(appName));

            this.appVersion = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(appVersion));

 

            string sDesc = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(desc));

            if (sDesc.ToLower().Equals("null"))

            {

                this.desc = "";

            }

            else

            {

                this.desc = "更新内容如下:\r\n" + sDesc;

            }

        }

 

        public void DownloadUpdateFile()

        {

            string url = Constants.RemoteUrl + callExeName + "/update.zip";

            var client = new System.Net.WebClient();

            client.DownloadProgressChanged += (sender, e) =>

            {

                UpdateProcess(e.BytesReceived, e.TotalBytesToReceive);

            };

            client.DownloadDataCompleted += (sender, e) =>

            {

                string zipFilePath = System.IO.Path.Combine(updateFileDir, "update.zip");

                byte[] data = e.Result;

                BinaryWriter writer = new BinaryWriter(new FileStream(zipFilePath, FileMode.OpenOrCreate));

                writer.Write(data);

                writer.Flush();

                writer.Close();

 

                System.Threading.ThreadPool.QueueUserWorkItem((s) =>

                {

                    Action f = () =>

                    {

                       txtProcess.Text = "开始更新程序...";

                    };

                    this.Dispatcher.Invoke(f);

 

                    string tempDir = System.IO.Path.Combine(updateFileDir, "temp");

                    if (!Directory.Exists(tempDir))

                    {

                        Directory.CreateDirectory(tempDir);

                    }

                    UnZipFile(zipFilePath, tempDir);

 

                    //移动文件

                    //App

                    if(Directory.Exists(System.IO.Path.Combine(tempDir,"App")))

                    {

                        CopyDirectory(System.IO.Path.Combine(tempDir,"App"),appDir);

                    }

 

                    f = () =>

                    {

                        txtProcess.Text = "更新完成!";

 

                        try

                        {

                            //清空缓存文件夹

                            string rootUpdateDir = updateFileDir.Substring(0, updateFileDir.LastIndexOf(System.IO.Path.DirectorySeparatorChar));

                            foreach (string p in System.IO.Directory.EnumerateDirectories(rootUpdateDir))

                            {

                                if (!p.ToLower().Equals(updateFileDir.ToLower()))

                                {

                                    System.IO.Directory.Delete(p, true);

                                }

                            }

                        }

                        catch (Exception ex)

                        {

                            //MessageBox.Show(ex.Message);

                        }

 

                    };

                    this.Dispatcher.Invoke(f);

 

                    try

                    {

                        f = () =>

                        {

                            AlertWin alert = new AlertWin("更新完成,是否现在启动软件?") { WindowStartupLocation = WindowStartupLocation.CenterOwner, Owner = this };

                            alert.Title = "更新完成";

                            alert.Loaded += (ss, ee) =>

                            {

                                alert.YesButton.Width = 40;

                                alert.NoButton.Width = 40;

                            };

                            alert.Width=300;

                            alert.Height = 200;

                            alert.ShowDialog();

                            if (alert.YesBtnSelected)

                            {

                                //启动软件

                                string exePath = System.IO.Path.Combine(appDir, callExeName + ".exe");

                                var info = new System.Diagnostics.ProcessStartInfo(exePath);

                                info.UseShellExecute = true;

                                info.WorkingDirectory = appDir;// exePath.Substring(0, exePath.LastIndexOf(System.IO.Path.DirectorySeparatorChar));

                                System.Diagnostics.Process.Start(info);

                            }

                            else

                            {

 

                            }

                            this.Close();

                        };

                        this.Dispatcher.Invoke(f);

                    }

                    catch (Exception ex)

                    {

                        //MessageBox.Show(ex.Message);

                    }

                });

 

            };

            client.DownloadDataAsync(new Uri(url));

        }

 

        private static void UnZipFile(string zipFilePath, string targetDir)

        {

            ICCEmbedded.SharpZipLib.Zip.FastZipEvents evt = new ICCEmbedded.SharpZipLib.Zip.FastZipEvents();

            ICCEmbedded.SharpZipLib.Zip.FastZip fz = new ICCEmbedded.SharpZipLib.Zip.FastZip(evt);

            fz.ExtractZip(zipFilePath, targetDir, "");

        }

 

        public void UpdateProcess(long current, long total)

        {

            string status = (int)((float)current * 100 / (float)total) + "%";

            this.txtProcess.Text = status;

            rectProcess.Width = ((float)current / (float)total) * bProcess.ActualWidth;

        }

 

        public void CopyDirectory(string sourceDirName, string destDirName)

        {

            try

            {

                if (!Directory.Exists(destDirName))

                {

                    Directory.CreateDirectory(destDirName);

                    File.SetAttributes(destDirName, File.GetAttributes(sourceDirName));

                }

                if (destDirName[destDirName.Length - 1] != Path.DirectorySeparatorChar)

                    destDirName = destDirName + Path.DirectorySeparatorChar;

                string[] files = Directory.GetFiles(sourceDirName);

                foreach (string file in files)

                {

                    File.Copy(file, destDirName + Path.GetFileName(file), true);

                    File.SetAttributes(destDirName + Path.GetFileName(file), FileAttributes.Normal);

                }

                string[] dirs = Directory.GetDirectories(sourceDirName);

                foreach (string dir in dirs)

                {

                    CopyDirectory(dir, destDirName + Path.GetFileName(dir));

                }

            }

            catch (Exception ex)

            {

                throw new Exception("复制文件错误");

            }

        }

    }

  注:

  压缩解压用到开源库SharpZipLib,官网: http://www.icsharpcode.net/OpenSource/SharpZipLib/Download.aspx

  服务器上升级包的目录层次应该如下(假如要升级的运用程序为Test.exe):

  Test(与exe的名字相同)

  ----update.xml

  ----update.zip

  update.zip包用如下方式生成:

  新建一个目录APP,将所用升级的文件拷贝到APP目录下,然后压缩APP文件夹为update.zip文件

  升级服务器的路径配置写到Constants.cs类中。

  使用方法如下,在要升级的运用程序项目的Main函数中,加上一行语句:

  Ezhu.AutoUpdater.Updater.CheckUpdateStatus();

  到此,一款简单的自动升级组件就完成了!