半年前,我辭掉朝八晚十的工作,告別研發部的兄弟和前台MM,意氣風發地着手開發自己的服裝ERP。之所以這么有魄力,是因為我對當前市場上幾個主流服裝軟件頗不以為然,掂量着在服裝企業干過的這幾年,心說再不瘋狂就太對不起當初放棄寫字樓選擇進廠房的自己了。
於是開始沒日沒夜地敲鍵盤,經歷無數困惑、失望、憤怒、迷茫、愉悅、興奮,多次大規模項目重構,0次的異性約會之后,到如今產品的分銷部分終於基本成型。這兩天在梳理代碼的過程中,覺得有必要將一些心得體會記錄下來。該記錄會形成一個系列,但並不會系統,屬於揀哪說哪(話說第一篇我原本想寫點關於打印方面的知識)。
一兩年前,或更早以前,Ajax風靡全球,歷時長久的BS/CS之爭似乎可以蓋棺定論,當時我遇到的幾乎所有程序員都在孜孜不倦地談論着瀏覽器上的那檔子事。時至今日,BS應用仍然比CS更能迎合程序員的口味。不過主模塊架構我依然選擇CS模式,理由我就不贅述了。什么?非得給個說法?那我就陳列若干理由如下:
- 瀏覽器不是操作系統。微軟可以將HTML5和JS移入操作系統,卻不能將C#移入瀏覽器。互聯網發展將出現越來越多的應用,總有一天臃腫的瀏覽器會不堪重負,新的應用將只能依靠更多樣的其它技術平台。你說Silverlight?這玩意我一直不看好,雖然我用WPF好久,雖然WPF程序轉成Silverlight應用號稱非常簡單,但是我從來沒去研究過Silverlight。Silverlight的前景也確實不甚光明。
- 隨着網速的提高,我估計CS中Client的概念也將模糊。未來的應用對於客戶端來說,也許就是一個快捷圖標,而指向的地址是服務器,應用程序不需要安裝,只是在需要的時候實時下載到客戶端。
- 上述兩條太空泛,也很容易被噴。如果站在客戶的角度,CS更有可能實現他們眾多的“無理要求”。對於服裝系統來說,BS適合數據展現(現在的領導都喜歡拿個IPAD在那算利潤,咱對IOS是外行,只能從瀏覽器上下手)。
- ……
我認為CS的缺點主要在於安裝和升級,前者只能寄希望於上述第2條,咱們可以努力解決的就是版本升級。好的升級功能需要包含以下幾點:
- 版本發布工具
- 在線自動升級
- 運行時手動升級(可選擇升級版本)
- 不同客戶不同版本
- 可以設置是否強制升級
- 版本列表查看
- 版本還原(最多只能還原至最近強制升級版本)
- 升級失敗后回滾,並讓用戶選擇直接運行程序or重新升級
- 刪除過期文件
- ……
上述紅字表示暫時未開發。本着通用的原則,我建了幾個產品無關的類。
這幾個類簡單明了,無需多做解釋,我們需要的是一個工具來維護它們,下面是其中一個界面的截圖,由於開發倉促,該工具並不完善(我自己用用足矣)。
不完善的其中一個點我已經在圖中注明,該點產生的原因是由於待更新文件列表支持文件夾,如<filelist><file name="fileA.dll"><directory path="dirA\childDirA"></filelist>,由於涉及到文件夾,路徑問題就出現了,完善該需求需要提供軟件發布根目錄信息和選擇文件和文件夾的樹形結構選擇器。另外這工具並不能說是真正意義上的版本發布,因為它沒有提供上傳文件到服務器的功能。
當用戶運行軟件時,升級程序(另外一個小程序,專門用於處理版本升級)啟動,檢查軟件配置文件記錄的當前版本,並與數據庫中的版本記錄作一比較,若有強制升級的新版本則自動升級。需要注意的是可能新發布了多個版本,那么我們就要合並重復的文件。以下為主要代碼:
1 /// <summary>
2 /// 獲取需要升級的文件和目錄 3 /// </summary>
4 /// <param name="softPath">待升級軟件的路徑</param>
5 private FilesNeedUpdate GetFilesNeedUpdate() 6 { 7 if (UpdateSection == null) 8 return null; 9 DataSet ds = null; 10 using (ChannelFactory<IVersionService> channelFactory = new ChannelFactory<IVersionService>("VersionSVC")) 11 { 12 IVersionService service = channelFactory.CreateChannel(); 13 //ds = service.GetFilesNeedUpdate(Path.GetFileName(softPath), GetNowVersion(softPath));
14 ds = service.GetFilesNeedUpdate(UpdateSection.CustomerKey, UpdateSection.SoftKey, UpdateSection.Version); 15 } 16 DataTable dt = ds.Tables[0]; 17 if (dt.Rows.Count == 0) 18 return null; 19 if (!IsCoerciveUpdate(dt)) 20 return null; 21 UpdateSection.Version = dt.Rows[0]["VersionCode"].ToString(); 22 List<FileNeedUpdate> files = new List<FileNeedUpdate>(); 23 List<FileNeedUpdate> directories = new List<FileNeedUpdate>(); 24 XmlDocument doc = new XmlDocument(); 25 foreach (DataRow row in dt.Rows) 26 { 27 doc.LoadXml(row["UpdatedFileList"].ToString()); 28 var tempFiles = GetNodeNameList(doc.GetElementsByTagName("file")).ToList(); 29 var tempDires = GetNodeNameList(doc.GetElementsByTagName("directory")).ToList(); 30 files = Coverforward(tempFiles, files); 31 directories = Coverforward(tempDires, directories); 32 } 33 return new FilesNeedUpdate { Files = files.ToArray(), Directories = directories.ToArray() }; 34 } 35
36 private IEnumerable<FileNeedUpdate> GetNodeNameList(XmlNodeList nodes) 37 { 38 foreach (XmlNode node in nodes) 39 { 40 var name = node.Attributes["name"].Value; 41 FileNeedUpdate item = new FileNeedUpdate { Name = name }; 42 var dnode = node.Attributes["isDelete"]; 43 if (dnode != null) 44 item.IsDelete = Convert.ToBoolean(dnode.Value); 45 yield return item; 46 } 47 } 48
49 /// <summary>
50 /// 前向覆蓋 51 /// </summary>
52 private List<FileNeedUpdate> Coverforward(List<FileNeedUpdate> filesFormer, List<FileNeedUpdate> filesAfter) 53 { 54 var diff = filesFormer.Except(filesAfter); 55 filesAfter.AddRange(diff); 56 return filesAfter; 57 } 58
59 /// <summary>
60 /// 是否強制更新 61 /// </summary>
62 private bool IsCoerciveUpdate(DataTable dt) 63 { 64 foreach (var row in dt.Rows) 65 { 66 if (Convert.ToBoolean(dt.Rows[0]["IsCoerciveUpdate"])) 67 return true; 68 } 69 return false; 70 }
獲取待更新的文件集合后,就可以去服務器端下載了(文件事先上傳到服務器)。為了節省帶寬(用戶可不想浪費太多干正事的時間),先將這些文件在服務器端壓縮后再下載,下載完畢后在客戶端解壓,並將服務器端的壓縮文件刪除。這塊我使用了ICSharpCode.SharpZipLib.dll,挺好用的,就不做贅述了。讓人頭大的是在升級過程中的消息提示需要各種異步,特別在WPF中,由於WPF沒有提供強制刷新界面的方法(有間接方式,但並不推薦),在某些方面令人牙疼。關於WPF中的“異步”編程,我會在以后做一總結。
1 public partial class MainWindow : Window 2 { 3 private WebClient _client; 4 private string _zipFileName, _mainApp, _bkZipFilePath; 5 private Dispatcher _dispatcher; 6 private FilesNeedUpdate _files; 7 private UpdateHelper _helper; 8
9 public MainWindow() 10 { 11 InitializeComponent(); 12 _dispatcher = this.Dispatcher; 13 _client = new WebClient(); 14 _client.DownloadProgressChanged += new DownloadProgressChangedEventHandler(client_DownloadProgressChanged); 15 _client.DownloadFileCompleted += new AsyncCompletedEventHandler(client_DownloadFileCompleted); 16 _client.Proxy = WebRequest.DefaultWebProxy; 17 _client.Proxy.Credentials = new NetworkCredential(); 18 } 19
20 public MainWindow(UpdateHelper helper) 21 : this() 22 { 23 _mainApp = helper.SoftPath; 24 _files = helper.Files; 25 _helper = helper; 26 this.Loaded += new RoutedEventHandler(MainWindow_Loaded); 27 } 28
29 void MainWindow_Loaded(object sender, RoutedEventArgs e) 30 { 31 try
32 { 33 LoadingLabel.Text = "有新版本發布,正在備份當前文件,請稍候……"; 34 Action bkaction = () => BackUpFiles(); 35 bkaction.BeginInvoke(new AsyncCallback(HandleFilesToUpdate), bkaction); 36 } 37 catch (Exception ex) 38 { 39 HandleException(ex); 40 } 41 } 42
43 private void HandleException(Exception e) 44 { 45 _dispatcher.Invoke(new Action(() =>
46 { 47 tbError.Text = "系統升級出錯,錯誤原因:" + e.Message; 48 pnlError.Visibility = Visibility.Visible; 49 })); 50 } 51
52 void Init() 53 { 54 pnlError.Visibility = Visibility.Collapsed; 55 this.RadProgressBar1.Value = 0; 56 PercentageLabel.Text = ""; 57 if (!string.IsNullOrEmpty(_bkZipFilePath) && File.Exists(_bkZipFilePath)) 58 File.Delete(_bkZipFilePath); 59 _zipFileName = _bkZipFilePath = ""; 60 } 61
62 private void HandleFilesToUpdate(IAsyncResult res) 63 { 64 Action action = new Action(() =>
65 { 66 try
67 { 68 DeleteAbateFiles(); 69 var filesNeedDownload = new FilesNeedUpdate 70 { 71 Files = _files.Files.ToList().FindAll(o => !o.IsDelete).ToArray(), 72 Directories = _files.Directories.ToList().FindAll(o => !o.IsDelete).ToArray() 73 }; 74 if (!filesNeedDownload.IsEmpty) 75 StartDownload(filesNeedDownload); 76 else
77 ReStartMainApp(); 78 _helper.SaveNewVersion(); 79 } 80 catch (Exception ex) 81 { 82 HandleException(ex); 83 } 84 }); 85 action.BeginInvoke(null, null); 86 } 87
88 private bool ArrayIsEmpty(Array array) 89 { 90 return array == null || array.Length == 0; 91 } 92
93 private void StartDownload(FilesNeedUpdate files) 94 { 95 //var section = _helper.UpdateSection; 96 //if (section == null || string.IsNullOrEmpty(section.SoftKey)) 97 //{ 98 // ReStartMainApp(); 99 //}
100 _dispatcher.Invoke(new Action(() =>
101 { 102 LoadingLabel.Text = "新版本文件遠程壓縮中……"; 103 }));//DispatcherPriority.SystemIdle:先繪制完界面再執行這段邏輯
104 string url; 105 using (ChannelFactory<IVersionService> channelFactory = new ChannelFactory<IVersionService>("VersionSVC")) 106 { 107 IVersionService service = channelFactory.CreateChannel(); 108 _zipFileName = service.CompressFilesNeedUpdate(files); 109 url = service.GetFilesUpdateUrl(_helper.UpdateSection.SoftKey); 110 } 111 //var url = ConfigurationManager.AppSettings["VersionFileUrl"];
112 if (!url.EndsWith("/")) 113 url += "/"; 114 url += _zipFileName; 115 _dispatcher.Invoke(new Action(() =>
116 { 117 //將壓縮文件下載到臨時文件夾
118 LoadingLabel.Text = "新版本文件下載中……"; 119 })); 120 _client.DownloadFileAsync(new Uri(url), GetTempFolder() + "\\" + _zipFileName); 121 } 122
123 /// <summary>
124 /// 獲取下載文件夾地址及解壓文件存放地址 125 /// 此地址默認為C:\Documents and Settings\當前用戶名\Local Settings\Temp 文件夾 126 /// </summary>
127 private string GetTempFolder() 128 { 129 string folder = System.Environment.GetEnvironmentVariable("TEMP"); 130 return new DirectoryInfo(folder).FullName; 131 } 132
133 void client_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e) 134 { 135 _dispatcher.BeginInvoke(new Action(() =>
136 { 137 this.RadProgressBar1.Value = e.ProgressPercentage; 138 PercentageLabel.Text = e.ProgressPercentage.ToString() + " %"; 139 })); 140 } 141
142 void client_DownloadFileCompleted(object sender, AsyncCompletedEventArgs e) 143 { 144 _dispatcher.Invoke(new Action(() =>
145 { 146 LoadingLabel.Text = PercentageLabel.Text = ""; 147 CompleteLabel.Text = "文件接收完成,正在更新……"; 148 })); 149 HandleUploadedFiles(); 150 } 151
152 private void HandleUploadedFiles() 153 { 154 FilesHandler.UnpackFiles(GetTempFolder() + "\\" + _zipFileName, this.GetAppRootPath()); 155
156 using (ChannelFactory<IVersionService> channelFactory = new ChannelFactory<IVersionService>("VersionSVC")) 157 { 158 IVersionService service = channelFactory.CreateChannel(); 159 service.DeleteCompressedFile(_zipFileName); 160 } 161 ReStartMainApp(); 162 } 163
164 /// <summary>
165 /// 刪除已過期的文件 166 /// </summary>
167 private void DeleteAbateFiles() 168 { 169 var filesNeedDelete = new FilesNeedUpdate 170 { 171 Files = _files.Files.ToList().FindAll(o => o.IsDelete).ToArray(), 172 Directories = _files.Directories.ToList().FindAll(o => o.IsDelete).ToArray() 173 }; 174 if (!filesNeedDelete.IsEmpty) 175 { 176 _dispatcher.Invoke(new Action(() =>
177 { 178 CompleteLabel.Text = "正在刪除已過期文件……"; 179 }), DispatcherPriority.Normal); 180 FilesHandler.DeleteFiles(filesNeedDelete.Files.Select(o => o.Name).ToArray(), filesNeedDelete.Directories.Select(o => o.Name).ToArray(), _mainApp); 181 } 182
183 } 184
185 private void ReStartMainApp(IAsyncResult res = null) 186 { 187 _dispatcher.Invoke(new Action(() =>
188 { 189 CompleteLabel.Text = "正在重啟應用程序,請稍候……"; 190 })); 191
192 _dispatcher.BeginInvoke(new Action(() =>
193 { 194 Process.Start(_mainApp); 195 this.Init(); 196 Process.GetCurrentProcess().Kill(); 197 }), DispatcherPriority.SystemIdle); 198 } 199
200 private string GetAppRootPath() 201 { 202 var rootPath = System.IO.Path.GetDirectoryName(_mainApp); 203 if (!rootPath.EndsWith("\\")) 204 rootPath += "\\"; 205 return rootPath; 206 } 207
208 //更新前備份文件
209 private void BackUpFiles() 210 { 211 var rootPath = GetAppRootPath(); 212 _bkZipFilePath = rootPath + Guid.NewGuid().ToString() + ".zip"; 213 FilesHandler.CompressFiles(_files, rootPath, _bkZipFilePath); 214 } 215 }
升級完畢后不要忘記保存新版本編號到配置文件。
1 internal void SaveNewVersion() 2 { 3 UpdateSection.CurrentConfiguration.Save(ConfigurationSaveMode.Modified); 4 }
這里的UpdateSection定義如下:
1 public class UpdateOnlineSection : ConfigurationSection 2 { 3 [ConfigurationProperty("CustomerKey", DefaultValue = "")] 4 public string CustomerKey 5 { 6 get { return (string)base["CustomerKey"]; } 7 set { base["CustomerKey"] = value; } 8 } 9
10 [ConfigurationProperty("SoftKey", DefaultValue = "")] 11 public string SoftKey 12 { 13 get { return (string)base["SoftKey"]; } 14 set { base["SoftKey"] = value; } 15 } 16
17 [ConfigurationProperty("Version", DefaultValue = "")] 18 public string Version 19 { 20 get { return (string)base["Version"]; } 21 set { base["Version"] = value; } 22 } 23 }
對應的是主程序的版本節點。自定義配置節點有兩種方式,繼承IConfigurationSectionHandler或繼承自ConfigurationSection,由於我們要從外部程序(此處是升級程序)訪問主程序的配置,必須繼承自ConfigurationSection方可。
主程序使用WCF獲取版本信息。代碼就不貼了,下面給個界面截圖。
改進點:需要在該界面上增加手動升級的按鈕,以及當前運行版本標示。
文至此,想到尚有一些業務需求未完成,再無下筆欲望,若有朋友感興趣,我會在空閑時間將該功能涉及到的幾個工具完善后剝離出來提供下載。
轉載本文請注明出處:http://www.cnblogs.com/newton/archive/2013/01/12/2857722.html