c# 智慧升級程式程式碼

iDotNetSpace發表於2009-07-13

最近單位開發一個專案,其中需要用到自動升級功能。因為自動升級是一個比較常用的功能,可能會在很多程式中用到,於是,我就想寫一個自動升級的元件,在應用程式中,只需要引用這個自動升級元件,並新增少量程式碼,即可實現自動升級功能。因為我們的程式中可能包含多個exe或者dll檔案,所以要支援多檔案的更新。

首先,要確定程式應該去哪裡下載需要升級的檔案。我選擇了到指定的網站上去下載,這樣比較簡單,也通用一些。在這個網站上,需要放置一個當前描述最新檔案列表的檔案,我們估且叫它伺服器配置檔案。這個檔案儲存了當前最新檔案的版本號(lastver),大小(size),下載地址(url),本地檔案的儲存路徑(path),還有當更新了這個檔案後,程式是否需要重新啟動(needRestart)。這個檔案大致如下:
updateservice.xml 


  http://update.iyond.com/CompanyClientApplication/AutoUpdater.zip" lastver="1.0.0.0" size="28672" needRestart="true" />
  http://update.iyond.com/CompanyClientApplication/CompanyClient.zip" lastver="1.1.0.0" size="888832 " needRestart="true" />
  http://update.iyond.com/CompanyClientApplication/HappyFenClient.zip" lastver="1.0.0.0" size="24576" needRestart="true" />
  http://update.iyond.com/CompanyClientApplication/NetworkProvider.zip" lastver="1.0.0.0" size="32768" needRestart="true" />
  http://update.iyond.com/CompanyClientApplication/Utility.zip" lastver="1.0.0.0" size="20480" needRestart="true" />
  http://update.iyond.com/CompanyClientApplication/Wizard.zip" lastver="1.0.0.0" size="24576"  needRestart="true" />

同時,客戶端也儲存了一個需要升級的本地檔案的列表,形式和伺服器配置檔案差不多,我們叫它本地配置檔案。其中,節點表示是否啟用自動升級功能,表示伺服器配置檔案的地址。
update.config 

http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  true
  http://update.iyond.com/updateservice.xml
 
   
   
   
   
   
   
 

使用自動各級元件的程式在啟動時,會去檢查這個配置檔案。如果發現有配置檔案中的檔案版本和本地配置檔案中描述的檔案版本不一致,則提示使用者下載。同時,如果本地配置檔案中某些檔案在伺服器配置檔案的檔案列表中不存在,則說明這個檔案已經不需要了,需要刪除。最後,當升級完成後,會更新本地配置檔案。

我們先來看一下如何使用這個元件。
在程式的Program.cs的Main函式中:

[STAThread]
static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    AutoUpdater au = new AutoUpdater();
    try
    {
        au.Update();
    }
    catch (WebException exp)
    {
        MessageBox.Show(String.Format("無法找到指定資源\n\n{0}", exp.Message), "自動升級", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
    catch (XmlException exp)
    {
        MessageBox.Show(String.Format("下載的升級檔案有錯誤\n\n{0}", exp.Message), "自動升級", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
    catch (NotSupportedException exp)
    {
        MessageBox.Show(String.Format("升級地址配置錯誤\n\n{0}", exp.Message), "自動升級", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
    catch (ArgumentException exp)
    {
        MessageBox.Show(String.Format("下載的升級檔案有錯誤\n\n{0}", exp.Message), "自動升級", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
    catch (Exception exp)
    {
        MessageBox.Show(String.Format("升級過程中發生錯誤\n\n{0}", exp.Message), "自動升級", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }

    Application.Run(new MainUI());
}

如上所示,只需要簡單的幾行程式碼,就可以實現自動升級功能了。

軟體執行截圖:

下面,我們來詳細說一下這個自動升級元件的實現。
先看一下類圖:

AutoUpdater:自動升級的管理類,負責整體的自動升級功能的實現。
Config:配置類,負責管理本地配置檔案。
DownloadConfirm:一個對話方塊,向使用者顯示需要升級的檔案的列表,並允許使用者選擇是否馬上升級。
DownloadFileInfo:要下載的檔案的資訊
DownloadProgress:一個對話方塊,顯示下載進度。
DownloadProgress.ExitCallBack,
DownloadProgress.SetProcessBarCallBack,
DownloadProgress.ShowCurrentDownloadFileNameCallBack:由於.NET2.0不允許在一個執行緒中訪問另一個執行緒的物件,所以需要通過委託來實現。
LocalFile:表示本地配置檔案中的一個檔案
RemoteFile:表示伺服器配置檔案中的一個檔案。
UpdateFileList:一個集合,從List繼承

我們先整體看一下AutoUpdater.cs:

public class AutoUpdater
{
    const string FILENAME = "update.config";
    private Config config = null;
    private bool bNeedRestart = false;

    public AutoUpdater()
    {
        config = Config.LoadConfig(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, FILENAME));
    }
    /**/
    ///


    /// 檢查新版本
    ///

    /// 無法找到指定資源
    /// 升級地址配置錯誤
    /// 下載的升級檔案有錯誤
    /// 下載的升級檔案有錯誤
    /// 未知錯誤
    ///
    public void Update()
    {
        if (!config.Enabled)
            return;
        /**/
        /*
     * 請求Web伺服器,得到當前最新版本的檔案列表,格式同本地的FileList.xml。
     * 與本地的FileList.xml比較,找到不同版本的檔案
     * 生成一個更新檔案列表,開始DownloadProgress
     *
     *
     *

     * path為相對於應用程式根目錄的相對目錄位置,包括檔名
     */
        WebClient client = new WebClient();
        string strXml = client.DownloadString(config.ServerUrl);

        Dictionary listRemotFile = ParseRemoteXml(strXml);

        List downloadList = new List();

        //某些檔案不再需要了,刪除
        List preDeleteFile = new List();

        foreach (LocalFile file in config.UpdateFileList)
        {
            if (listRemotFile.ContainsKey(file.Path))
            {
                RemoteFile rf = listRemotFile[file.Path];
                if (rf.LastVer != file.LastVer)
                {
                    downloadList.Add(new DownloadFileInfo(rf.Url, file.Path, rf.LastVer, rf.Size));
                    file.LastVer = rf.LastVer;
                    file.Size = rf.Size;

                    if (rf.NeedRestart)
                        bNeedRestart = true;
                }

                listRemotFile.Remove(file.Path);
            }
            else
            {
                preDeleteFile.Add(file);
            }
        }

        foreach (RemoteFile file in listRemotFile.Values)
        {
            downloadList.Add(new DownloadFileInfo(file.Url, file.Path, file.LastVer, file.Size));
            config.UpdateFileList.Add(new LocalFile(file.Path, file.LastVer, file.Size));

            if (file.NeedRestart)
                bNeedRestart = true;
        }

        if (downloadList.Count > 0)
        {
            DownloadConfirm dc = new DownloadConfirm(downloadList);

            if (this.OnShow != null)
                this.OnShow();

            if (DialogResult.OK == dc.ShowDialog())
            {
                foreach (LocalFile file in preDeleteFile)
                {
                    string filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, file.Path);
                    if (File.Exists(filePath))
                        File.Delete(filePath);

                    config.UpdateFileList.Remove(file);
                }

                StartDownload(downloadList);
            }
        }
    }

    private void StartDownload(List downloadList)
    {
        DownloadProgress dp = new DownloadProgress(downloadList);
        if (dp.ShowDialog() == DialogResult.OK)
        {
            //更新成功
            config.SaveConfig(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, FILENAME));

            if (bNeedRestart)
            {
                MessageBox.Show("程式需要重新啟動才能應用更新,請點選確定重新啟動程式。", "自動更新", MessageBoxButtons.OK, MessageBoxIcon.Information);
                Process.Start(Application.ExecutablePath);
                Environment.Exit(0);
            }
        }
    }

    private Dictionary ParseRemoteXml(string xml)
    {
        XmlDocument document = new XmlDocument();
        document.LoadXml(xml);

        Dictionary list = new Dictionary();
        foreach (XmlNode node in document.DocumentElement.ChildNodes)
        {
            list.Add(node.Attributes["path"].Value, new RemoteFile(node));
        }

        return list;
    }
    public event ShowHandler OnShow;
}

在建構函式中,我們先要載入配置檔案:

public AutoUpdater()
{
    config = Config.LoadConfig(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, FILENAME));
}

最主要的就是Update()這個函式了。當程式呼叫au.Update時,首先檢查當前是否開戶了自動更新: 
if (!config.Enabled)
    return;

如果啟用了自動更新,就需要去下載伺服器配置檔案了: 
WebClient client = new WebClient();
string strXml = client.DownloadString(config.ServerUrl);

然後,解析伺服器配置檔案到一個Dictionary中:

Dictionary listRemotFile = ParseRemoteXml(strXml);

接下來比較伺服器配置檔案和本地配置檔案,找出需要下載的檔案和本地需要刪除的檔案:

List downloadList = new List();
//某些檔案不再需要了,刪除
List preDeleteFile = new List();

foreach (LocalFile file in config.UpdateFileList)
{
    if (listRemotFile.ContainsKey(file.Path))
    {
        RemoteFile rf = listRemotFile[file.Path];
        if (rf.LastVer != file.LastVer)
        {
            downloadList.Add(new DownloadFileInfo(rf.Url, file.Path, rf.LastVer, rf.Size));
            file.LastVer = rf.LastVer;
            file.Size = rf.Size;

            if (rf.NeedRestart)
                bNeedRestart = true;
        }

        listRemotFile.Remove(file.Path);
    }
    else
    {
        preDeleteFile.Add(file);
    }
}

foreach (RemoteFile file in listRemotFile.Values)
{
    downloadList.Add(new DownloadFileInfo(file.Url, file.Path, file.LastVer, file.Size));
    config.UpdateFileList.Add(new LocalFile(file.Path, file.LastVer, file.Size));

    if (file.NeedRestart)
        bNeedRestart = true;
}

如果發現有需要下載的檔案,則向使用者顯示這些檔案,並提示其是否馬上更新。如果使用者選擇了馬上更新,則先刪除本地不再需要的檔案,然後開始下載更新檔案。 
if (downloadList.Count > 0)
{
    DownloadConfirm dc = new DownloadConfirm(downloadList);

    if (this.OnShow != null)
        this.OnShow();

    if (DialogResult.OK == dc.ShowDialog())
    {
        foreach (LocalFile file in preDeleteFile)
        {
            string filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, file.Path);
            if (File.Exists(filePath))
                File.Delete(filePath);

            config.UpdateFileList.Remove(file);
        }

        StartDownload(downloadList);
    }
}

我們再來看一下StartDownload函式

private void StartDownload(List downloadList)
{
    DownloadProgress dp = new DownloadProgress(downloadList);
    if (dp.ShowDialog() == DialogResult.OK)
    {
        //更新成功
        config.SaveConfig(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, FILENAME));

        if (bNeedRestart)
        {
            MessageBox.Show("程式需要重新啟動才能應用更新,請點選確定重新啟動程式。", "自動更新", MessageBoxButtons.OK, MessageBoxIcon.Information);
            Process.Start(Application.ExecutablePath);
            Environment.Exit(0);
        }
    }
}

在這個函式中,先呼叫DownloadProgress下載所有需要下載的檔案,然後更新本地配置檔案,最後,如果發現某些更新檔案需要重新啟動應用程式的話,會提示使用者重新啟動程式。

至此,AutoUpdater這個類的使命就完成了,其實,整個的升級過程也就完成了。(廢話)。

最後,我們來看一下這個元件是如何下載更新檔案的

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;
using System.Net;
using System.IO;
using System.Diagnostics;

namespace Iyond.Utility
{
    public partial class DownloadProgress : Form
    {
        private bool isFinished = false;
        private List downloadFileList = null;
        private ManualResetEvent evtDownload = null;
        private ManualResetEvent evtPerDonwload = null;
        private WebClient clientDownload = null;

        public DownloadProgress(List downloadFileList)
        {
            InitializeComponent();

            this.downloadFileList = downloadFileList;
        }

        private void OnFormClosing(object sender, FormClosingEventArgs e)
        {
            if (!isFinished && DialogResult.No == MessageBox.Show("當前正在更新,是否取消?", "自動升級", MessageBoxButtons.YesNo, MessageBoxIcon.Question))
            {
                e.Cancel = true;
                return;
            }
            else
            {
                if (clientDownload != null)
                    clientDownload.CancelAsync();

                evtDownload.Set();
                evtPerDonwload.Set();
            }
        }

        private void OnFormLoad(object sender, EventArgs e)
        {
            evtDownload = new ManualResetEvent(true);
            evtDownload.Reset();
            Thread t = new Thread(new ThreadStart(ProcDownload));
            t.Name = "download";
            t.Start();
        }

        long total = 0;
        long nDownloadedTotal = 0;

        private void ProcDownload()
        {
            evtPerDonwload = new ManualResetEvent(false);

            foreach (DownloadFileInfo file in this.downloadFileList)
            {
                total += file.Size;
            }

            while (!evtDownload.WaitOne(0, false))
            {
                if (this.downloadFileList.Count == 0)
                    break;

                DownloadFileInfo file = this.downloadFileList[0];

                //Debug.WriteLine(String.Format("Start Download:{0}", file.FileName));

                this.ShowCurrentDownloadFileName(file.FileName);

                //下載
                clientDownload = new WebClient();

                clientDownload.DownloadProgressChanged += new DownloadProgressChangedEventHandler(OnDownloadProgressChanged);
                clientDownload.DownloadFileCompleted += new AsyncCompletedEventHandler(OnDownloadFileCompleted);

                evtPerDonwload.Reset();

                clientDownload.DownloadFileAsync(new Uri(file.DownloadUrl), Path.Combine(AppDomain.CurrentDomain.BaseDirectory, file.FileFullName + ".tmp"), file);

                //等待下載完成
                evtPerDonwload.WaitOne();

                clientDownload.Dispose();
                clientDownload = null;

                //移除已下載的檔案
                this.downloadFileList.Remove(file);
            }

            //Debug.WriteLine("All Downloaded");

            if (this.downloadFileList.Count == 0)
                Exit(true);
            else
                Exit(false);

            evtDownload.Set();
        }

        void OnDownloadFileCompleted(object sender, AsyncCompletedEventArgs e)
        {
            DownloadFileInfo file = e.UserState as DownloadFileInfo;
            nDownloadedTotal += file.Size;
            this.SetProcessBar(0, (int)(nDownloadedTotal * 100 / total));
            //Debug.WriteLine(String.Format("Finish Download:{0}", file.FileName));
            //替換現有檔案
            string filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, file.FileFullName);
            if (File.Exists(filePath))
            {
                if (File.Exists(filePath + ".old"))
                    File.Delete(filePath + ".old");

                File.Move(filePath, filePath + ".old");
            }

            File.Move(filePath + ".tmp", filePath);
            //繼續下載其它檔案
            evtPerDonwload.Set();
        }

        void OnDownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
        {
            this.SetProcessBar(e.ProgressPercentage, (int)((nDownloadedTotal + e.BytesReceived) * 100 / total));
        }

        delegate void ShowCurrentDownloadFileNameCallBack(string name);
        private void ShowCurrentDownloadFileName(string name)
        {
            if (this.labelCurrentItem.InvokeRequired)
            {
                ShowCurrentDownloadFileNameCallBack cb = new ShowCurrentDownloadFileNameCallBack(ShowCurrentDownloadFileName);
                this.Invoke(cb, new object[] { name });
            }
            else
            {
                this.labelCurrentItem.Text = name;
            }
        }

        delegate void SetProcessBarCallBack(int current, int total);
        private void SetProcessBar(int current, int total)
        {
            if (this.progressBarCurrent.InvokeRequired)
            {
                SetProcessBarCallBack cb = new SetProcessBarCallBack(SetProcessBar);
                this.Invoke(cb, new object[] { current, total });
            }
            else
            {
                this.progressBarCurrent.Value = current;
                this.progressBarTotal.Value = total;
            }
        }

        delegate void ExitCallBack(bool success);
        private void Exit(bool success)
        {
            if (this.InvokeRequired)
            {
                ExitCallBack cb = new ExitCallBack(Exit);
                this.Invoke(cb, new object[] { success });
            }
            else
            {
                this.isFinished = success;
                this.DialogResult = success ? DialogResult.OK : DialogResult.Cancel;
                this.Close();
            }
        }

        private void OnCancel(object sender, EventArgs e)
        {
            evtDownload.Set();
            evtPerDonwload.Set();
        }
    }
}

在建構函式中,將要下載的檔案列表傳進來

public DownloadProgress(List downloadFileList)
{
    InitializeComponent();

    this.downloadFileList = downloadFileList;
}

在Form的Load事件中,啟動下載執行緒,開始下載。

private void OnFormLoad(object sender, EventArgs e)
{
    evtDownload = new ManualResetEvent(true);
    evtDownload.Reset();
    Thread t = new Thread(new ThreadStart(ProcDownload));
    t.Name = "download";
    t.Start();
}

下載執行緒沒什麼特殊的,使用了WebClient的非同步下載檔案函式DownloadFileAsync,並且註冊了兩個事件,分別負責下載進度顯示和下載完成後的處理: 
clientDownload.DownloadProgressChanged += new DownloadProgressChangedEventHandler(OnDownloadProgressChanged);
clientDownload.DownloadFileCompleted += new AsyncCompletedEventHandler(OnDownloadFileCompleted);

大家看一下就明白了。

private void ProcDownload()
{
    evtPerDonwload = new ManualResetEvent(false);

    foreach (DownloadFileInfo file in this.downloadFileList)
    {
        total += file.Size;
    }

    while (!evtDownload.WaitOne(0, false))
    {
        if (this.downloadFileList.Count == 0)
            break;

        DownloadFileInfo file = this.downloadFileList[0];

        //Debug.WriteLine(String.Format("Start Download:{0}", file.FileName));

        this.ShowCurrentDownloadFileName(file.FileName);

        //下載
        clientDownload = new WebClient();

        clientDownload.DownloadProgressChanged += new DownloadProgressChangedEventHandler(OnDownloadProgressChanged);
        clientDownload.DownloadFileCompleted += new AsyncCompletedEventHandler(OnDownloadFileCompleted);

        evtPerDonwload.Reset();

        clientDownload.DownloadFileAsync(new Uri(file.DownloadUrl), Path.Combine(AppDomain.CurrentDomain.BaseDirectory, file.FileFullName + ".tmp"), file);

        //等待下載完成
        evtPerDonwload.WaitOne();

        clientDownload.Dispose();
        clientDownload = null;

        //移除已下載的檔案
        this.downloadFileList.Remove(file);
    }

    //Debug.WriteLine("All Downloaded");

    if (this.downloadFileList.Count == 0)
        Exit(true);
    else
        Exit(false);

    evtDownload.Set();
}

最後,在OnDownloadFileCompleted函式中進行最後的處理。包括備份原檔案,替換現有檔案等。 
void OnDownloadFileCompleted(object sender, AsyncCompletedEventArgs e)
{
    DownloadFileInfo file = e.UserState as DownloadFileInfo;
    nDownloadedTotal += file.Size;
    this.SetProcessBar(0, (int)(nDownloadedTotal * 100 / total));
    //Debug.WriteLine(String.Format("Finish Download:{0}", file.FileName));
    //替換現有檔案
    string filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, file.FileFullName);
    if (File.Exists(filePath))
    {
        if (File.Exists(filePath + ".old"))
            File.Delete(filePath + ".old");

        File.Move(filePath, filePath + ".old");
    }

    File.Move(filePath + ".tmp", filePath);
    //繼續下載其它檔案
    evtPerDonwload.Set();
}

其它的函式只是一些顯示進度條和下載資訊的,這裡就不再詳細介紹了。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-608971/,如需轉載,請註明出處,否則將追究法律責任。

相關文章