前言
在桌面端开发中,传统的软件启动检测下载压缩包-覆盖安装”早已落伍。优秀的用户体验应该是:程序启动后在后台悄悄完成新版本检测与下载,等用户准备好了,再通过一个弹窗温柔地提醒:“新版本已就绪,是否现在重启更新?”
本文将基于 C# WinForms (.NET Framework 4.5.2),实现这一整套流程。
一、 核心逻辑设计
我们的热更新逻辑分为四个阶段:
后台检测:程序启动后,开启异步线程请求服务器版本信息。
静默下载:发现新版本后,在后台下载更新包(通常是 .zip 或 .exe),不干扰用户操作。
用户决策:下载完成后,弹窗询问:
-
立即更新:关闭当前程序,启动更新脚本,替换文件并重启。
-
下次更新:记录标记位,待用户下次正常启动程序时再执行替换。
文件替换:解决“正在运行的 EXE 无法被覆盖”的核心痛点。
二、 技术准备
1. 解决 .NET 4.5.2 的 HTTPS 握手问题
.NET 4.5.2 默认不启用 TLS 1.2。如果你的更新包托管在 GitHub 或 HTTPS 服务器上,直接下载会报错。 必须在程序启动时执行:
// 在 Program.cs 的 Main 方法或主窗体构造函数中添加
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
2. 服务器端结构
你需要一个 version.xml 或 JSON 放在服务器上,例如:
<?xml version="1.0" encoding="utf-8"?>
<UpdateInfo>
<Version>1.1.0</Version>
<Url>http://yourserver.com/patch/v1.1.0.zip</Url>
<Description>修复了已知Bug,优化了UI体验。</Description>
</UpdateInfo>
三、 核心代码实现
第一步:后台版本检测与下载管理
我们创建一个 UpdateManager 类来管理所有逻辑。
using System;
using System.IO;
using System.Net;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Xml.Linq;
public class UpdateManager
{
private string _updateUrl = "http://yourserver.com/update.xml"; // 服务器配置地址
private string _tempZipPath = Path.Combine(Application.StartupPath, "temp_update.zip");
public string NewVersion { get; private set; }
// 检查更新
public async Task CheckAndUpdateAsync()
{
try
{
// 1. 获取本地版本
Version currentVersion = Assembly.GetExecutingAssembly().GetName().Version;
// 2. 异步下载服务器版本信息
WebClient client = new WebClient();
string xmlContent = await client.DownloadStringTaskAsync(_updateUrl);
var doc = XDocument.Parse(xmlContent);
string remoteVersionStr = doc.Element("UpdateInfo").Element("Version").Value;
string downloadUrl = doc.Element("UpdateInfo").Element("Url").Value;
Version remoteVersion = new Version(remoteVersionStr);
// 3. 比对版本
if (remoteVersion > currentVersion)
{
NewVersion = remoteVersionStr;
// 开始静默下载更新包
await DownloadUpdateAsync(downloadUrl);
}
}
catch (Exception ex)
{
// 记录日志,但不干扰用户使用程序
Console.WriteLine("检查更新失败: " + ex.Message);
}
}
private async Task DownloadUpdateAsync(string url)
{
using (WebClient client = new WebClient())
{
// 在后台下载 zip 压缩包
await client.DownloadFileTaskAsync(new Uri(url), _tempZipPath);
// 下载完成后,通知 UI 线程
NotifyUser();
}
}
private void NotifyUser()
{
// 这里可以使用事件通知主界面,或者直接弹出对话框
DialogResult result = MessageBox.Show(
$"新版本 {NewVersion} 已准备就绪!\\n是否立即重启更新?\\n(取消则在下次启动时更新)",
"更新提醒",
MessageBoxButtons.YesNo,
MessageBoxIcon.Information);
if (result == DialogResult.Yes)
{
ApplyUpdateNow();
}
else
{
// 下次重启更新的逻辑:可以在本地写一个标记文件
File.WriteAllText(Path.Combine(Application.StartupPath, "pending_update.flag"), "true");
}
}
public void ApplyUpdateNow()
{
// 关键逻辑:启动外部更新脚本,并关闭当前程序
string scriptPath = Path.Combine(Application.StartupPath, "Updater.bat");
// 创建一个简单的批处理脚本来执行替换
CreateUpdateScript(scriptPath);
System.Diagnostics.Process.Start(scriptPath);
Application.Exit();
}
private void CreateUpdateScript(string path)
{
// 批处理逻辑:等待主程序退出 -> 解压覆盖 -> 重启程序 -> 自删除
string exeName = AppDomain.CurrentDomain.FriendlyName;
string script = $@"
@echo off
taskkill /f /im {exeName} > nul
timeout /t 2 /nobreak > nul
:: 假设你使用了简单的解压工具或手动拷贝文件
:: 这里简单演示文件替换
move /y temp_update.zip old_update.zip
echo Updating…
:: 此处通常调用一个小的 Updater.exe 来解压,或者用 PowerShell
powershell -command ""Expand-Archive -Path 'temp_update.zip' -DestinationPath '.' -Force""
start {exeName}
del ""{path}""
";
File.WriteAllText(path, script, System.Text.Encoding.Default);
}
}
四、 进阶技巧:如何解决“正在运行的 EXE 无法删除”?
很多开发者会写一个专门的 Updater.exe。但其实有一个**“文件更名大法”**:
原理: 在 Windows 中,你不能覆盖一个正在运行的 .exe,但你可以重命名它。[1]
将当前正在运行的 MyApp.exe 重命名为 MyApp.exe.old。
将下载好的新版 MyApp.exe 放到原位置。
下次启动时,新版程序检测是否存在 .old 文件,如果有则顺手删掉。[2]
这种方式不需要中间脚本,几乎可以实现无感切换。
五、 在 WinForms 主窗体中调用
public partial class MainForm : Form
{
private UpdateManager _updManager = new UpdateManager();
public MainForm()
{
InitializeComponent();
this.Load += MainForm_Load;
}
private async void MainForm_Load(object sender, EventArgs e)
{
// 1. 启动后静默检测
// 注意:千万不要在这里 Wait(),否则会卡死 UI
await _updManager.CheckAndUpdateAsync();
// 2. 检测是否存在待处理的更新(针对“下次重启更新”的情况)
CheckPendingUpdate();
}
private void CheckPendingUpdate()
{
string flagPath = Path.Combine(Application.StartupPath, "pending_update.flag");
if (File.Exists(flagPath))
{
File.Delete(flagPath);
// 提示用户:发现上次下载好的更新,现在应用吗?
_updManager.ApplyUpdateNow();
}
}
}
希望这篇博客对你有帮助!如果有任何细节需要深入探讨,欢迎随时提问。




