一、引言
對於C/S架構來說,軟件更新是一個很常用的功能,下面介紹一種非常實用的軟件自動升級方案。
二、示意圖
三、項目說明
3.1、項目創建
新建4個項目,如下所示:
3.2、項目關系
四、LinkTo.Toolkit
LinkTo.Toolkit主要是一些Utility及Helper類文件,實現轉換擴展、文件讀寫、進程處理等功能。

/// <summary> /// 轉換擴展類 /// </summary> public static class ConvertExtension { public static string ToString2(this object obj) { if (obj == null) return string.Empty; return obj.ToString(); } public static DateTime? ToDateTime(this string str) { if (string.IsNullOrEmpty(str)) return null; if (DateTime.TryParse(str, out DateTime dateTime)) { return dateTime; } return null; } public static bool ToBoolean(this string str) { if (string.IsNullOrEmpty(str)) return false; return str.ToLower() == bool.TrueString.ToLower(); } public static bool IsNullOrEmpty(this string str) { return string.IsNullOrEmpty(str); } public static int ToInt(this string str) { if (int.TryParse(str, out int intValue)) { return intValue; } return 0; } public static long ToLong(this string str) { if (long.TryParse(str, out long longValue)) { return longValue; } return 0; } public static decimal ToDecimal(this string str) { if (decimal.TryParse(str, out decimal decimalValue)) { return decimalValue; } return 0; } public static double ToDouble(this string str) { if (double.TryParse(str, out double doubleValue)) { return doubleValue; } return 0; } public static float ToFloat(this string str) { if (float.TryParse(str, out float floatValue)) { return floatValue; } return 0; } /// <summary> /// DataRow轉換為實體類 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dr"></param> /// <returns></returns> public static T ConvertToEntityByDataRow<T>(this DataRow dataRow) where T : new() { Type type = typeof(T); PropertyInfo[] properties = type.GetProperties(); T t = new T(); if (dataRow == null) return t; foreach (PropertyInfo property in properties) { foreach (DataColumn column in dataRow.Table.Columns) { if (property.Name.Equals(column.ColumnName, StringComparison.OrdinalIgnoreCase)) { object value = dataRow[column]; if (value != null && value != DBNull.Value) { if (value.GetType().Name != property.PropertyType.Name) { if (property.PropertyType.IsEnum) { property.SetValue(t, Enum.Parse(property.PropertyType, value.ToString()), null); } else { try { value = Convert.ChangeType(value, (Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType)); property.SetValue(t, value, null); } catch { } } } else { property.SetValue(t, value, null); } } else { property.SetValue(t, null, null); } break; } } } return t; } /// <summary> /// 通用簡單實體類型互轉 /// </summary> public static T ConvertToEntity<T>(this object sourceEntity) where T : new() { T t = new T(); Type sourceType = sourceEntity.GetType(); if (sourceType.Equals(typeof(DataRow))) { //DataRow類型 DataRow dataRow = sourceEntity as DataRow; t = dataRow.ConvertToEntityByDataRow<T>(); } else { Type type = typeof(T); PropertyInfo[] properties = type.GetProperties(); PropertyInfo[] sourceProperties = sourceType.GetProperties(); foreach (PropertyInfo property in properties) { foreach (var sourceProperty in sourceProperties) { if (sourceProperty.Name.Equals(property.Name, StringComparison.OrdinalIgnoreCase)) { object value = sourceProperty.GetValue(sourceEntity, null); if (value != null && value != DBNull.Value) { if (sourceProperty.PropertyType.Name != property.PropertyType.Name) { if (property.PropertyType.IsEnum) { property.SetValue(t, Enum.Parse(property.PropertyType, value.ToString()), null); } else { try { value = Convert.ChangeType(value, (Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType)); property.SetValue(t, value, null); } catch { } } } else { property.SetValue(t, value, null); } } else { property.SetValue(t, null, null); } break; } } } } return t; } /// <summary> /// 通用簡單實體類型互轉 /// </summary> public static List<T> ConvertToEntityList<T>(this object list) where T : new() { List<T> t = new List<T>(); if (list == null) return t; Type sourceObj = list.GetType(); if (sourceObj.Equals(typeof(DataTable))) { var dataTable = list as DataTable; t = dataTable.Rows.Cast<DataRow>().Where(m => !(m.RowState == DataRowState.Deleted || m.RowState == DataRowState.Detached)).Select(m => m.ConvertToEntityByDataRow<T>()).ToList(); } else if (list is IEnumerable) { t = ((IList)list).Cast<object>().Select(m => m.ConvertToEntity<T>()).ToList(); } return t; } /// <summary> /// 轉換為DataTable,如果是集合沒有數據行的時候會拋異常。 /// </summary> /// <param name="list"></param> /// <returns></returns> public static DataTable ConvertToDataTable(this object list) { if (list == null) return null; DataTable dataTable = new DataTable(); if (list is IEnumerable) { var li = (IList)list; //li[0]代表的是一個對象,list沒有行時,會拋異常。 PropertyInfo[] properties = li[0].GetType().GetProperties(); dataTable.Columns.AddRange(properties.Where(m => !m.PropertyType.IsClass || !m.PropertyType.IsInterface).Select(m => new DataColumn(m.Name, Nullable.GetUnderlyingType(m.PropertyType) ?? m.PropertyType)).ToArray()); foreach (var item in li) { DataRow dataRow = dataTable.NewRow(); foreach (PropertyInfo property in properties.Where(m => m.PropertyType.GetProperty("Item") == null)) //過濾含有索引器的屬性 { object value = property.GetValue(item, null); dataRow[property.Name] = value ?? DBNull.Value; } dataTable.Rows.Add(dataRow); } } else { PropertyInfo[] properties = list.GetType().GetProperties(); properties = properties.Where(m => m.PropertyType.GetProperty("Item") == null).ToArray(); //過濾含有索引器的屬性 dataTable.Columns.AddRange(properties.Select(m => new DataColumn(m.Name, Nullable.GetUnderlyingType(m.PropertyType) ?? m.PropertyType)).ToArray()); DataRow dataRow = dataTable.NewRow(); foreach (PropertyInfo property in properties) { object value = property.GetValue(list, null); dataRow[property.Name] = value ?? DBNull.Value; } dataTable.Rows.Add(dataRow); } return dataTable; } /// <summary> /// 實體類公共屬性值復制 /// </summary> /// <param name="entity"></param> /// <param name="target"></param> public static void CopyTo(this object entity, object target) { if (target == null) return; if (entity.GetType() != target.GetType()) return; PropertyInfo[] properties = target.GetType().GetProperties(); foreach (PropertyInfo property in properties) { if (property.PropertyType.GetProperty("Item") != null) continue; object value = property.GetValue(entity, null); if (value != null) { if (value is ICloneable) { property.SetValue(target, (value as ICloneable).Clone(), null); } else { property.SetValue(target, value.Copy(), null); } } else { property.SetValue(target, null, null); } } } public static object Copy(this object obj) { if (obj == null) return null; object targetDeepCopyObj; Type targetType = obj.GetType(); if (targetType.IsValueType == true) { targetDeepCopyObj = obj; } else { targetDeepCopyObj = Activator.CreateInstance(targetType); //創建引用對象 MemberInfo[] memberCollection = obj.GetType().GetMembers(); foreach (MemberInfo member in memberCollection) { if (member.GetType().GetProperty("Item") != null) continue; if (member.MemberType == MemberTypes.Field) { FieldInfo field = (FieldInfo)member; object fieldValue = field.GetValue(obj); if (fieldValue is ICloneable) { field.SetValue(targetDeepCopyObj, (fieldValue as ICloneable).Clone()); } else { field.SetValue(targetDeepCopyObj, fieldValue.Copy()); } } else if (member.MemberType == MemberTypes.Property) { PropertyInfo property = (PropertyInfo)member; MethodInfo method = property.GetSetMethod(false); if (method != null) { object propertyValue = property.GetValue(obj, null); if (propertyValue is ICloneable) { property.SetValue(targetDeepCopyObj, (propertyValue as ICloneable).Clone(), null); } else { property.SetValue(targetDeepCopyObj, propertyValue.Copy(), null); } } } } } return targetDeepCopyObj; } }

public class FileHelper { private readonly string strUpdateFilesPath; public FileHelper(string strDirector) { strUpdateFilesPath = strDirector; } //保存所有的文件信息 private List<FileInfo> listFiles = new List<FileInfo>(); public List<FileInfo> GetAllFilesInDirectory(string strDirector) { DirectoryInfo directory = new DirectoryInfo(strDirector); DirectoryInfo[] directoryArray = directory.GetDirectories(); FileInfo[] fileInfoArray = directory.GetFiles(); if (fileInfoArray.Length > 0) listFiles.AddRange(fileInfoArray); foreach (DirectoryInfo item in directoryArray) { DirectoryInfo directoryA = new DirectoryInfo(item.FullName); DirectoryInfo[] directoryArrayA = directoryA.GetDirectories(); GetAllFilesInDirectory(item.FullName); } return listFiles; } public string[] GetUpdateList(List<FileInfo> listFileInfo) { var fileArrary = listFileInfo.Cast<FileInfo>().Select(s => s.FullName.Replace(strUpdateFilesPath, "")).ToArray(); return fileArrary; } /// <summary> /// 刪除文件夾下的所有文件但不刪除目錄 /// </summary> /// <param name="dirRoot"></param> public static void DeleteDirAllFile(string dirRoot) { DirectoryInfo directoryInfo = new DirectoryInfo(Path.GetDirectoryName(dirRoot)); FileInfo[] files = directoryInfo.GetFiles("*.*", SearchOption.AllDirectories); foreach (FileInfo item in files) { File.Delete(item.FullName); } } }

public static class FileUtility { #region 讀取文件 /// <summary> /// 讀取文件 /// </summary> /// <param name="filePath">文件路徑</param> /// <returns></returns> public static string ReadFile(string filePath) { string result = string.Empty; if (File.Exists(filePath) == false) { return result; } try { using (var streamReader = new StreamReader(filePath, Encoding.UTF8)) { result = streamReader.ReadToEnd(); } } catch (Exception) { result = string.Empty; } return result; } #endregion 讀文件 #region 寫入文件 /// <summary> /// 寫入文件 /// </summary> /// <param name="filePath">文件路徑</param> /// <param name="strValue">寫入內容</param> /// <returns></returns> public static bool WriteFile(string filePath, string strValue) { try { if (File.Exists(filePath) == false) { using (FileStream fileStream = File.Create(filePath)) { } } using (var streamWriter = new StreamWriter(filePath, true, Encoding.UTF8)) { streamWriter.WriteLine(strValue); } return true; } catch (Exception) { return false; } } #endregion #region 刪除文件 /// <summary> /// 刪除文件 /// </summary> /// <param name="filePath">文件路徑</param> /// <returns></returns> public static bool DeleteFile(string filePath) { try { File.Delete(filePath); return true; } catch (Exception) { return false; } } #endregion 刪除文件 #region 為文件添加用戶組的完全控制權限 /// <summary> /// 為文件添加用戶組的完全控制權限 /// </summary> /// <param name="userGroup">用戶組</param> /// <param name="filePath">文件路徑</param> /// <returns></returns> public static bool AddSecurityControll2File(string userGroup, string filePath) { try { //獲取文件信息 FileInfo fileInfo = new FileInfo(filePath); //獲得該文件的訪問權限 FileSecurity fileSecurity = fileInfo.GetAccessControl(); //添加用戶組的訪問權限規則--完全控制權限 fileSecurity.AddAccessRule(new FileSystemAccessRule(userGroup, FileSystemRights.FullControl, AccessControlType.Allow)); //設置訪問權限 fileInfo.SetAccessControl(fileSecurity); //返回結果 return true; } catch (Exception) { //返回結果 return false; } } #endregion #region 為文件夾添加用戶組的完全控制權限 /// <summary> /// 為文件夾添加用戶組的完全控制權限 /// </summary> /// <param name="userGroup">用戶組</param> /// <param name="dirPath">文件夾路徑</param> /// <returns></returns> public static bool AddSecurityControll2Folder(string userGroup,string dirPath) { try { //獲取文件夾信息 DirectoryInfo dir = new DirectoryInfo(dirPath); //獲得該文件夾的所有訪問權限 DirectorySecurity dirSecurity = dir.GetAccessControl(AccessControlSections.All); //設定文件ACL繼承 InheritanceFlags inherits = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit; //添加用戶組的訪問權限規則--完全控制權限 FileSystemAccessRule usersFileSystemAccessRule = new FileSystemAccessRule(userGroup, FileSystemRights.FullControl, inherits, PropagationFlags.None, AccessControlType.Allow); dirSecurity.ModifyAccessRule(AccessControlModification.Add, usersFileSystemAccessRule, out bool isModified); //設置訪問權限 dir.SetAccessControl(dirSecurity); //返回結果 return true; } catch (Exception) { //返回結果 return false; } } #endregion }

public static class ProcessUtility { #region 關閉進程 /// <summary> /// 關閉進程 /// </summary> /// <param name="processName">進程名</param> public static void KillProcess(string processName) { Process[] myproc = Process.GetProcesses(); foreach (Process item in myproc) { if (item.ProcessName == processName) { item.Kill(); } } } #endregion }

/// <summary> /// Xml序列化與反序列化 /// </summary> public static class XmlUtility { #region 序列化 /// <summary> /// 序列化 /// </summary> /// <param name="type">類型</param> /// <param name="obj">對象</param> /// <returns></returns> public static string Serializer(Type type, object obj) { MemoryStream Stream = new MemoryStream(); XmlSerializer xml = new XmlSerializer(type); try { //序列化對象 xml.Serialize(Stream, obj); } catch (InvalidOperationException) { throw; } Stream.Position = 0; StreamReader sr = new StreamReader(Stream); string str = sr.ReadToEnd(); sr.Dispose(); Stream.Dispose(); return str; } #endregion 序列化 #region 反序列化 /// <summary> /// 反序列化 /// </summary> /// <param name="type">類型</param> /// <param name="xml">XML字符串</param> /// <returns></returns> public static object Deserialize(Type type, string xml) { try { using (StringReader sr = new StringReader(xml)) { XmlSerializer xmldes = new XmlSerializer(type); return xmldes.Deserialize(sr); } } catch (Exception ex) { return ex.Message; } } /// <summary> /// 反序列化 /// </summary> /// <param name="type"></param> /// <param name="xml"></param> /// <returns></returns> public static object Deserialize(Type type, Stream stream) { XmlSerializer xmldes = new XmlSerializer(type); return xmldes.Deserialize(stream); } #endregion 反序列化 }
五、AutoUpdaterTest
5.1、實體類
作用:本地配置AutoUpdateConfig.xml文件的序列化及反序列化實體對象。

public class AutoUpdateConfig { /// <summary> /// 自動升級模式:當前僅支持HTTP /// </summary> public string AutoUpdateMode { get; set; } /// <summary> /// HTTP自動升級模式時的URL地址 /// </summary> public string AutoUpdateHttpUrl { get; set; } }
5.2、通用類
作用:應用程序全局靜態常量。全局參數都在此設置,方便統一管理。注:客戶端是否檢測更新,也是在此設置默認值。

/// <summary> /// 應用程序全局靜態常量 /// </summary> public static class GlobalParam { #region 自動更新參數 /// <summary> /// 是否檢查自動更新:默認是true /// </summary> public static string CheckAutoUpdate = "true"; /// <summary> /// 本地自動更新配置XML文件名 /// </summary> public const string AutoUpdateConfig_XmlFileName = "AutoUpdateConfig.xml"; /// <summary> /// 本地自動更新下載臨時存放目錄 /// </summary> public const string TempDir = "Temp"; /// <summary> /// 遠端自動更新信息XML文件名 /// </summary> public const string AutoUpdateInfo_XmlFileName = "AutoUpdateInfo.xml"; /// <summary> /// 遠端自動更新文件存放目錄 /// </summary> public const string RemoteDir = "AutoUpdateFiles"; /// <summary> /// 主線程名 /// </summary> public const string MainProcess = "AutoUpdaterTest"; #endregion }
作用:應用程序上下文。

/// <summary> /// 應用程序上下文 /// </summary> public class AppContext { /// <summary> /// 客戶端配置文件 /// </summary> public static AutoUpdateConfig AutoUpdateConfigData { get; set; } }
作用:應用程序配置。

public class AppConfig { private static readonly object _lock = new object(); private static AppConfig _instance = null; #region 自動更新配置 /// <summary> /// 自動更新配置數據 /// </summary> public AutoUpdateConfig AutoUpdateConfigData { get; set; } private AppConfig() { AutoUpdateConfigData = new AutoUpdateConfig(); } public static AppConfig Instance { get { if (_instance == null) { lock (_lock) { if (_instance == null) { _instance = new AppConfig(); } } } return _instance; } } /// <summary> /// 本地自動更新下載臨時文件夾路徑 /// </summary> public string TempPath { get { return string.Format("{0}\\{1}", Application.StartupPath, GlobalParam.TempDir); } } /// <summary> /// 初始化系統配置信息 /// </summary> public void InitialSystemConfig() { AutoUpdateConfigData.AutoUpdateMode = AppContext.AutoUpdateConfigData.AutoUpdateMode; AutoUpdateConfigData.AutoUpdateHttpUrl = AppContext.AutoUpdateConfigData.AutoUpdateHttpUrl; } #endregion }
5.3、工具類
作用:配置文件的讀寫。

public class AutoUpdateHelper { private readonly string AutoUpdateMode = string.Empty; public AutoUpdateHelper(string autoUpdateMode) { AutoUpdateMode = autoUpdateMode; } /// <summary> /// 加載本地自動更新配置文件 /// </summary> /// <returns></returns> public static AutoUpdateConfig Load() { string filePath = string.Empty, fileContent = string.Empty; filePath = Path.Combine(Application.StartupPath, GlobalParam.AutoUpdateConfig_XmlFileName); AutoUpdateConfig config = new AutoUpdateConfig(); fileContent = FileUtility.ReadFile(filePath); object obj = XmlUtility.Deserialize(typeof(AutoUpdateConfig), fileContent); config = obj as AutoUpdateConfig; return config; } /// <summary> /// 獲取遠端自動更新信息的版本號 /// </summary> /// <returns></returns> public string GetRemoteAutoUpdateInfoVersion() { XDocument doc = new XDocument(); doc = XDocument.Parse(GetRemoteAutoUpdateInfoXml()); return doc.Element("AutoUpdateInfo").Element("NewVersion").Value; } /// <summary> /// 獲取遠端自動更新信息的XML文件內容 /// </summary> /// <returns></returns> public string GetRemoteAutoUpdateInfoXml() { string remoteXmlAddress = AppConfig.Instance.AutoUpdateConfigData.AutoUpdateHttpUrl + "/" + GlobalParam.AutoUpdateInfo_XmlFileName; string receiveXmlPath = Path.Combine(AppConfig.Instance.TempPath, GlobalParam.AutoUpdateInfo_XmlFileName); string xmlString = string.Empty; if (Directory.Exists(AppConfig.Instance.TempPath) == false) { Directory.CreateDirectory(AppConfig.Instance.TempPath); } if (AutoUpdateMode.ToUpper() == "HTTP") { WebClient client = new WebClient(); client.DownloadFile(remoteXmlAddress, receiveXmlPath); } if (File.Exists(receiveXmlPath)) { xmlString = FileUtility.ReadFile(receiveXmlPath); return xmlString; } return string.Empty; } /// <summary> /// 寫入本地自動更新配置的XML文件內容 /// </summary> /// <returns></returns> public string WriteLocalAutoUpdateInfoXml() { string xmlPath = string.Empty, xmlValue = string.Empty; xmlPath = Path.Combine(AppConfig.Instance.TempPath, GlobalParam.AutoUpdateConfig_XmlFileName); xmlValue = XmlUtility.Serializer(typeof(AutoUpdateConfig), AppConfig.Instance.AutoUpdateConfigData); if (File.Exists(xmlPath)) { File.Delete(xmlPath); } bool blSuccess = FileUtility.WriteFile(xmlPath, xmlValue); return blSuccess == true ? xmlPath : ""; } }
5.4、本地配置文件
作用:配置自動更新模式及相關。
注1:復制到輸出目錄選擇始終復制。
注2:主程序運行時,先讀取此配置更新文件,然后給AppContext上下文賦值,接着給AppConfig配置賦值。

<?xml version="1.0" encoding="utf-8" ?> <AutoUpdateConfig> <!--自動升級模式:當前僅支持HTTP--> <AutoUpdateMode>HTTP</AutoUpdateMode> <!--HTTP自動升級模式時的URL地址--> <AutoUpdateHttpUrl>http://127.0.0.1:6600/AutoUpdateDir</AutoUpdateHttpUrl> </AutoUpdateConfig>
5.5、主程序
新建一個Windows 窗體MainForm,此處理僅需要一個空白窗體即可,作測試用。

public partial class MainForm : Form { private static MainForm _Instance; /// <summary> /// MainForm主窗體實例 /// </summary> public static MainForm Instance { get { if (_Instance == null) { _Instance = new MainForm(); } return _Instance; } } public MainForm() { InitializeComponent(); } }
5.6、應用程序主入口
作用:檢測應用程序是否需要自動更新,如里需要則檢測遠程服務端的版本號。假如遠程服務端有新版本,則調用自動更新器AutoUpdater並向其傳遞4個參數。

internal static class Program { /// <summary> /// 應用程序的主入口點 /// </summary> [STAThread] private static void Main(string[] args) { //嘗試設置訪問權限 FileUtility.AddSecurityControll2Folder("Users", Application.StartupPath); //未捕獲的異常處理 Application.ThreadException += Application_ThreadException; Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; //是否檢查自動更新賦值 if (args.Length > 0) { GlobalParam.CheckAutoUpdate = args[0]; } //加載自動更新配置文件,給上下文AppServiceConfig對象賦值。 var config = AutoUpdateHelper.Load(); AppContext.AutoUpdateConfigData = config; //窗體互斥體 var instance = new Mutex(true, GlobalParam.MainProcess, out bool isNewInstance); if (isNewInstance == true) { if (GlobalParam.CheckAutoUpdate.ToBoolean()) { if (CheckUpdater()) ProcessUtility.KillProcess(GlobalParam.MainProcess); } Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(MainForm.Instance); instance.ReleaseMutex(); } else { MessageBox.Show("已經啟動了一個程序,請先退出。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Error); Application.Exit(); } } /// <summary> /// 自動更新檢測 /// </summary> /// <returns></returns> private static bool CheckUpdater() { if (GlobalParam.CheckAutoUpdate.ToBoolean() == false) return false; #region 檢查版本更新 Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); bool blFinish = false; AppConfig.Instance.InitialSystemConfig(); var helper = new AutoUpdateHelper(AppConfig.Instance.AutoUpdateConfigData.AutoUpdateMode); string fileVersion = FileVersionInfo.GetVersionInfo(Application.ExecutablePath).FileVersion; long localVersion = 0; long remoteVersion = 0; try { localVersion = fileVersion.Replace(".", "").ToLong(); remoteVersion = helper.GetRemoteAutoUpdateInfoVersion().Replace(".", "").ToLong(); if ((localVersion > 0) && (localVersion < remoteVersion)) { blFinish = true; string autoUpdateConfigXmlPath = helper.WriteLocalAutoUpdateInfoXml(); string autoUpdateInfoXmlPath = Path.Combine(AppConfig.Instance.TempPath, GlobalParam.AutoUpdateInfo_XmlFileName); string argument1 = autoUpdateConfigXmlPath; string argument2 = autoUpdateInfoXmlPath; string argument3 = GlobalParam.MainProcess; string argument4 = GlobalParam.RemoteDir; string arguments = argument1 + " " + argument2 + " " + argument3 + " " + argument4; Process.Start("AutoUpdater.exe", arguments); Application.Exit(); } } catch (TimeoutException) { blFinish = false; } catch (WebException) { blFinish = false; } catch (Exception) { blFinish = false; } return blFinish; #endregion } /// <summary> /// UI線程未捕獲異常處理 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private static void Application_ThreadException(object sender, ThreadExceptionEventArgs e) { string strError = "", strLog = "", strDateInfo = DateTime.Now.ToString() + " 出現應用程序未處理的異常:\r\n"; var error = e.Exception; if (error != null) { strError = strDateInfo + $"異常類型:{error.GetType().Name}\r\n異常消息:{error.Message}"; strLog = strDateInfo + $"異常類型:{error.GetType().Name}\r\n異常消息:{error.Message}\r\n堆棧信息:{error.StackTrace}\r\n來源信息:{error.Source}\r\n"; } else { strError = $"Application ThreadException:{e}"; } WriteLog(strLog); MessageBox.Show(strError, "系統錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error); } /// <summary> /// 非UI線程未捕獲異常處理 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { string strError = "", strLog = "", strDateInfo = DateTime.Now.ToString() + " 出現應用程序未處理的異常:\r\n"; if (e.ExceptionObject is Exception error) { strError = strDateInfo + $"異常消息:{error.Message}"; strLog = strDateInfo + $"異常消息:{error.Message}\r\n堆棧信息:{error.StackTrace}"; } else { strError = $"Application UnhandledError:{e}"; } WriteLog(strLog); MessageBox.Show(strError, "系統錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error); } /// <summary> /// 寫入日志 /// </summary> /// <param name="strLog"></param> private static void WriteLog(string strLog) { string dirPath = @"Log\MainProcess", fileName = DateTime.Now.ToString("yyyy-MM-dd") + ".txt"; string strLine = "----------------------------------------------------------------------------------------------------"; FileUtility.WriteFile(Path.Combine(dirPath, fileName), strLog); FileUtility.WriteFile(Path.Combine(dirPath,fileName), strLine); } }
六、AutoUpdater
6.1、實體類
作用:配置自動更新模式及相關。

/// <summary> /// 自動更新配置信息 /// </summary> public class AutoUpdateConfig { /// <summary> /// 自動升級模式:當前僅支持HTTP /// </summary> public string AutoUpdateMode { get; set; } /// <summary> /// HTTP自動升級模式時的URL地址 /// </summary> public string AutoUpdateHttpUrl { get; set; } }
作用:自動更新內容信息。

/// <summary> /// 自動更新內容信息 /// </summary> [Serializable] public class AutoUpdateInfo { /// <summary> /// 新版本號 /// </summary> public string NewVersion { get; set; } /// <summary> /// 更新日期 /// </summary> public string UpdateTime { get; set; } /// <summary> /// 更新內容說明 /// </summary> public string UpdateContent { get; set; } /// <summary> /// 更新文件列表 /// </summary> public List<string> FileList { get; set; } }
6.2、通用類
作用:應用程序全局靜態常量。全局參數都在此設置,方便統一管理。

/// <summary> /// 應用程序全局靜態常量 /// </summary> public static class GlobalParam { /// <summary> /// 調用程序主線程名稱 /// </summary> public static string MainProcess = string.Empty; /// <summary> /// 遠程更新程序所在文件夾的名稱 /// </summary> public static string RemoteDir = string.Empty; }
6.3、Window 窗體
新建一個Windows 窗體HttpStartUp。

public partial class HttpStartUp : Form { private bool _blSuccess = false; private string _autoUpdateHttpUrl = null; private AutoUpdateInfo _autoUpdateInfo = null; public HttpStartUp(string autoUpdateHttpUrl, AutoUpdateInfo autoUpdateInfo) { InitializeComponent(); _autoUpdateHttpUrl = autoUpdateHttpUrl; _autoUpdateInfo = autoUpdateInfo; _blSuccess = false; } /// <summary> /// 窗體加載事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Main_Load(object sender, EventArgs e) { Text = GlobalParam.MainProcess + "-更新程序"; lblUpdateTime.Text = _autoUpdateInfo.UpdateTime; lblNewVersion.Text = _autoUpdateInfo.NewVersion; txtUpdate.Text = _autoUpdateInfo.UpdateContent; } /// <summary> /// 立即更新 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnRun_Click(object sender, EventArgs e) { ProcessUtility.KillProcess(GlobalParam.MainProcess); btnRun.Enabled = false; btnLeave.Enabled = false; Thread thread = new Thread(() => { try { var downFileList = _autoUpdateInfo.FileList.OrderByDescending(s => s.IndexOf("\\")); foreach (var fileName in downFileList) { string fileUrl = string.Empty, fileVaildPath = string.Empty; if (fileName.StartsWith("\\")) { fileVaildPath = fileName.Substring(fileName.IndexOf("\\")); } else { fileVaildPath = fileName; } fileUrl = _autoUpdateHttpUrl.TrimEnd(new char[] { '/' }) + @"/" + GlobalParam.RemoteDir + @"/" + fileVaildPath.Replace("\\", "/"); //替換文件目錄中的路徑為網絡路徑 DownloadFileDetail(fileUrl, fileName); } _blSuccess = true; } catch (Exception ex) { BeginInvoke(new MethodInvoker(() => { throw ex; })); } finally { BeginInvoke(new MethodInvoker(delegate () { btnRun.Enabled = true; btnLeave.Enabled = true; })); } if (_blSuccess) { Process.Start(GlobalParam.MainProcess + ".exe"); BeginInvoke(new MethodInvoker(delegate () { Close(); Application.Exit(); })); } }) { IsBackground = true }; thread.Start(); } private void DownloadFileDetail(string httpUrl, string filename) { string fileName = Application.StartupPath + "\\" + filename; string dirPath = GetDirPath(fileName); if (!Directory.Exists(dirPath)) { Directory.CreateDirectory(dirPath); } HttpWebRequest request = (HttpWebRequest)WebRequest.Create(httpUrl); HttpWebResponse response = (HttpWebResponse)request.GetResponse(); Stream httpStream = response.GetResponseStream(); long totalBytes = response.ContentLength; if (progressBar != null) { BeginInvoke(new MethodInvoker(delegate () { lblDownInfo.Text = "開始下載..."; progressBar.Maximum = (int)totalBytes; progressBar.Minimum = 0; })); } FileStream outputStream = new FileStream(fileName, FileMode.Create); int bufferSize = 2048; int readCount; byte[] buffer = new byte[bufferSize]; readCount = httpStream.Read(buffer, 0, bufferSize); int allByte = (int)response.ContentLength; int startByte = 0; BeginInvoke(new MethodInvoker(delegate () { progressBar.Maximum = allByte; progressBar.Minimum = 0; })); while (readCount > 0) { outputStream.Write(buffer, 0, readCount); readCount = httpStream.Read(buffer, 0, bufferSize); startByte += readCount; BeginInvoke(new MethodInvoker(delegate () { lblDownInfo.Text = "已下載:" + startByte / 1024 + "KB/" + "總長度:"+ allByte / 1024 + "KB" + " " + " 文件名:" + filename; progressBar.Value = startByte; })); Application.DoEvents(); Thread.Sleep(5); } BeginInvoke(new MethodInvoker(delegate () { lblDownInfo.Text = "下載完成。"; })); httpStream.Close(); outputStream.Close(); response.Close(); } public static string GetDirPath(string filePath) { if (filePath.LastIndexOf("\\") > 0) { return filePath.Substring(0, filePath.LastIndexOf("\\")); } return filePath; } /// <summary> /// 暫不更新 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnLeave_Click(object sender, EventArgs e) { if (MessageBox.Show("確定要放棄此次更新嗎?", "提示", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) { Process.Start(GlobalParam.MainProcess + ".exe", "false"); Close(); Application.Exit(); } } }
6.4、應用程序主入口

internal static class Program { /// <summary> /// 應用程序的主入口點 /// </summary> [STAThread] private static void Main(string[] args) { Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException); AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); #region 測試 //string strArgs = @"E:\LinkTo.AutoUpdater\AutoUpdaterTest\bin\Debug\Temp\AutoUpdateConfig.xml"+" "+@"E:\LinkTo.AutoUpdater\AutoUpdaterTest\bin\Debug\Temp\AutoUpdateInfo.xml"+" "+"AutoUpdaterTest"+" "+"AutoUpdateFiles"; //args = strArgs.Split(' '); #endregion if (args.Length > 0) { string autoUpdateConfigXmlPath = args[0].ToString(); string autoUpdateInfoXmlPath = args[1].ToString(); GlobalParam.MainProcess = args[2].ToString(); GlobalParam.RemoteDir = args[3].ToString(); var autoUpdateConfigXml = FileUtility.ReadFile(autoUpdateConfigXmlPath); var autoUpdateInfoXml = FileUtility.ReadFile(autoUpdateInfoXmlPath); AutoUpdateConfig config = XmlUtility.Deserialize(typeof(AutoUpdateConfig), autoUpdateConfigXml) as AutoUpdateConfig; AutoUpdateInfo info = XmlUtility.Deserialize(typeof(AutoUpdateInfo), autoUpdateInfoXml) as AutoUpdateInfo; if (config.AutoUpdateMode.ToUpper() == "HTTP") { Application.Run(new HttpStartUp(config.AutoUpdateHttpUrl, info)); } } else { Application.Exit(); } } /// <summary> /// UI線程未捕獲異常處理 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e) { string strError = "", strLog = "", strDateInfo = DateTime.Now.ToString() + " 出現應用程序未處理的異常:\r\n"; var error = e.Exception; if (error != null) { strError = strDateInfo + $"異常類型:{error.GetType().Name}\r\n異常消息:{error.Message}"; strLog = strDateInfo + $"異常類型:{error.GetType().Name}\r\n異常消息:{error.Message}\r\n堆棧信息:{error.StackTrace}\r\n來源信息:{error.Source}\r\n"; } else { strError = $"Application ThreadException:{e}"; } WriteLog(strLog); MessageBox.Show(strError, "系統錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error); } /// <summary> /// 非UI線程未捕獲異常處理 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { string strError = "", strLog = "", strDateInfo = DateTime.Now.ToString() + " 出現應用程序未處理的異常:\r\n"; if (e.ExceptionObject is Exception error) { strError = strDateInfo + $"異常消息:{error.Message}"; strLog = strDateInfo + $"異常消息:{error.Message}\r\n堆棧信息:{error.StackTrace}"; } else { strError = $"Application UnhandledError:{e}"; } WriteLog(strLog); MessageBox.Show(strError, "系統錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error); } /// <summary> /// 寫入日志 /// </summary> /// <param name="strLog"></param> private static void WriteLog(string strLog) { string dirPath = @"Log\AutoUpdater", fileName = DateTime.Now.ToString("yyyy-MM-dd") + ".txt"; string strLine = "----------------------------------------------------------------------------------------------------"; FileUtility.WriteFile(Path.Combine(dirPath, fileName), strLog); FileUtility.WriteFile(Path.Combine(dirPath, fileName), strLine); } }
七、AutoUpdateXmlBuilder
7.1、實體類
作用:自動更新內容信息。

/// <summary> /// 自動更新內容信息 /// </summary> [Serializable] public class AutoUpdateInfo { /// <summary> /// 新版本號 /// </summary> public string NewVersion { get; set; } /// <summary> /// 更新日期 /// </summary> public string UpdateTime { get; set; } /// <summary> /// 更新內容說明 /// </summary> public string UpdateContent { get; set; } /// <summary> /// 更新文件列表 /// </summary> public List<string> FileList { get; set; } }
7.2、通用類
作用:應用程序全局靜態常量。全局參數都在此設置,方便統一管理。

/// <summary> /// 應用程序全局靜態常量 /// </summary> public static class GlobalParam { /// <summary> /// 遠端自動更新信息XML文件名 /// </summary> public const string AutoUpdateInfo_XmlFileName = "AutoUpdateInfo.xml"; /// <summary> /// 遠端自動更新目錄 /// </summary> public const string AutoUpdateDir = "AutoUpdateDir"; /// <summary> /// 遠端自動更新文件存放目錄 /// </summary> public const string RemoteDir = "AutoUpdateFiles"; /// <summary> /// 主線程名 /// </summary> public const string MainProcess = "AutoUpdaterTest"; }
7.3、Window 窗體
1)新建一個Windows 窗體Main。

public partial class Main : Form { //自動更新目錄路徑 private static readonly string AutoUpdateDirPath = Application.StartupPath + @"\" + GlobalParam.AutoUpdateDir; //自動更新信息XML文件路徑 private static readonly string AutoUpdateInfoXmlPath = Path.Combine(AutoUpdateDirPath, GlobalParam.AutoUpdateInfo_XmlFileName); //自動更新文件目錄路徑 private static readonly string RemoteDirPath = Application.StartupPath + @"\" + GlobalParam.AutoUpdateDir + @"\" + GlobalParam.RemoteDir; public Main() { InitializeComponent(); } /// <summary> /// 窗體加載 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Main_Load(object sender, EventArgs e) { if (!Directory.Exists(RemoteDirPath)) { Directory.CreateDirectory(RemoteDirPath); } LoadBaseInfo(); LoadDirectoryFileList(); } /// <summary> /// 刷新 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnRefresh_Click(object sender, EventArgs e) { LoadBaseInfo(); LoadDirectoryFileList(); } /// <summary> /// 初始化 /// </summary> private void LoadBaseInfo() { dtUpdateTime.Text = DateTime.Now.ToString("yyyy-MM-dd"); txtNewVersion.Text = GetMainProcessFileVersion(); CreateHeaderAndFillListView(); } /// <summary> /// 獲取主程序文件版本 /// </summary> /// <returns></returns> private string GetMainProcessFileVersion() { string fileVersion = ""; if (File.Exists(RemoteDirPath + "\\" + GlobalParam.MainProcess + ".exe")) //如果更新中有主程序文件 { FileVersionInfo info = FileVersionInfo.GetVersionInfo(RemoteDirPath + "\\" + GlobalParam.MainProcess + ".exe"); fileVersion = info.FileVersion; } return fileVersion; } /// <summary> /// 添加ListView列名 /// </summary> private void CreateHeaderAndFillListView() { lstFileList.Columns.Clear(); int lvWithd = lstFileList.Width; ColumnHeader columnHeader; //First Header columnHeader = new ColumnHeader { Text = "#", Width = 38 }; lstFileList.Columns.Add(columnHeader); //Second Header columnHeader = new ColumnHeader { Text = "文件名", Width = (lvWithd - 38) / 2 }; lstFileList.Columns.Add(columnHeader); //Third Header columnHeader = new ColumnHeader { Text = "更新路徑", Width = (lvWithd - 38) / 2 }; lstFileList.Columns.Add(columnHeader); } /// <summary> /// 加載目錄文件列表 /// </summary> private void LoadDirectoryFileList() { if (!Directory.Exists(RemoteDirPath)) { Directory.CreateDirectory(RemoteDirPath); } FileHelper fileHelper = new FileHelper(RemoteDirPath); var fileArrary = fileHelper.GetUpdateList(fileHelper.GetAllFilesInDirectory(RemoteDirPath)).ToList(); var lastFile = fileArrary.FirstOrDefault(s => s == GlobalParam.MainProcess + ".exe"); //exe作為最后的文件更新,防止更新過程中出現網絡錯誤,導致文件未全部更新。 if (lastFile != null) { fileArrary.Remove(lastFile); fileArrary.Add(lastFile); } PopulateListViewWithArray(fileArrary.ToArray()); } /// <summary> /// 使用路徑字符數組填充列表 /// </summary> /// <param name="strArray"></param> private void PopulateListViewWithArray(string[] strArray) { lstFileList.Items.Clear(); if (strArray != null) { //只過濾根目錄下的特殊文件 strArray = strArray.Where(s => !new string[] { GlobalParam.AutoUpdateInfo_XmlFileName }.Contains(s.Substring(s.IndexOf('\\') + 1))).ToArray(); for (int i = 0; i < strArray.Length; i++) { ListViewItem lvi = new ListViewItem { Text = (i + 1).ToString() }; int intStart = strArray[i].LastIndexOf('\\') + 1; lvi.SubItems.Add(strArray[i].Substring(intStart, strArray[i].Length - intStart)); lvi.SubItems.Add(strArray[i]); lstFileList.Items.Add(lvi); } } } /// <summary> /// 生成更新XML文件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnBuild_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(txtNewVersion.Text)) { MessageBox.Show("更新版本號不能為空。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); txtNewVersion.Focus(); return; } if (string.IsNullOrEmpty(txtMainProcessName.Text)) { MessageBox.Show("主進程名不能為空。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); txtMainProcessName.Focus(); return; } AutoUpdateInfo info = new AutoUpdateInfo() { NewVersion = txtNewVersion.Text.Trim(), UpdateTime = dtUpdateTime.Value.ToString("yyyy-MM-dd"), UpdateContent = txtUpdateContent.Text, FileList = lstFileList.Items.Cast<ListViewItem>().Select(s => s.SubItems[2].Text).ToList() }; string xmlValue = XmlUtility.Serializer(typeof(AutoUpdateInfo), info); using (StreamWriter sw = new StreamWriter(AutoUpdateInfoXmlPath)) { sw.WriteLine(xmlValue); sw.Flush(); sw.Close(); } MessageBox.Show("生成成功。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); } /// <summary> /// 打開本地目錄 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnOpen_Click(object sender, EventArgs e) { ProcessStartInfo psi = new ProcessStartInfo("Explorer.exe") { Arguments = AutoUpdateDirPath }; Process.Start(psi); } }
2)在bin\Debug\下新建一個AutoUpdateDir文件夾,然后再在AutoUpdateDir下新建一個AutoUpdateFiles文件夾。
3)在AutoUpdaterTest中,將程序集版本及文件版本都改成1.0.0.1並重新生成,接着將AutoUpdaterTest.exe拷貝到AutoUpdateFiles下,最后將程序集版本及文件版本都改回1.0.0.0。
4)此時運行AutoUpdateXmlBuilder,點擊生成更新XML文件即打包成功。程序會自動在AutoUpdateDir下生成打包信息文件AutoUpdateInfo.xml。
7.4、應用程序主入口

internal static class Program { /// <summary> /// 應用程序的主入口點。 /// </summary> [STAThread] private static void Main() { Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException); AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Main()); } /// <summary> /// UI線程未捕獲異常處理 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e) { string strError = "", strLog = "", strDateInfo = DateTime.Now.ToString() + " 出現應用程序未處理的異常:\r\n"; var error = e.Exception; if (error != null) { strError = strDateInfo + $"異常類型:{error.GetType().Name}\r\n異常消息:{error.Message}"; strLog = strDateInfo + $"異常類型:{error.GetType().Name}\r\n異常消息:{error.Message}\r\n堆棧信息:{error.StackTrace}\r\n來源信息:{error.Source}\r\n"; } else { strError = $"Application ThreadException:{e}"; } WriteLog(strLog); MessageBox.Show(strError, "系統錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error); } /// <summary> /// 非UI線程未捕獲異常處理 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { string strError = "", strLog = "", strDateInfo = DateTime.Now.ToString() + " 出現應用程序未處理的異常:\r\n"; if (e.ExceptionObject is Exception error) { strError = strDateInfo + $"異常消息:{error.Message}"; strLog = strDateInfo + $"異常消息:{error.Message}\r\n堆棧信息:{error.StackTrace}"; } else { strError = $"Application UnhandledError:{e}"; } WriteLog(strLog); MessageBox.Show(strError, "系統錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error); } /// <summary> /// 寫入日志 /// </summary> /// <param name="strLog"></param> private static void WriteLog(string strLog) { string dirPath = @"Log\XmlBuilder", fileName = DateTime.Now.ToString("yyyy-MM-dd") + ".txt"; string strLine = "----------------------------------------------------------------------------------------------------"; FileUtility.WriteFile(Path.Combine(dirPath, fileName), strLog); FileUtility.WriteFile(Path.Combine(dirPath, fileName), strLine); } }
八、遠程服務端配置
注:此處為本機測試。
1)在某個盤符如E盤下新建一個AutoUpdate文件夾,將AutoUpdateXmlBuilder打包文件夾AutoUpdateDir拷貝到AutoUpdate文件夾下。
2)在IIS中新建一個網站,對應的虛擬目錄為E:\AutoUpdate,同時將端口設置為6600。
3)運行AutoUpdaterTest,如出現自動更新器即代表成功。