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();
到此,一款简单的自动升级组件就完成了!