天天看點

我的服裝DRP之線上更新

半年前,我辭掉朝八晚十的工作,告别研發部的兄弟和前台MM,意氣風發地着手開發自己的服裝ERP。之是以這麼有魄力,是因為我對目前市場上幾個主流服裝軟體頗不以為然,掂量着在服裝企業幹過的這幾年,心說再不瘋狂就太對不起當初放棄寫字樓選擇進廠房的自己了。

于是開始沒日沒夜地敲鍵盤,經曆無數困惑、失望、憤怒、迷茫、愉悅、興奮,多次大規模項目重構,0次的異性約會之後,到如今産品的分銷部分終于基本成型。這兩天在梳理代碼的過程中,覺得有必要将一些心得體會記錄下來。該記錄會形成一個系列,但并不會系統,屬于揀哪說哪(話說第一篇我原本想寫點關于列印方面的知識)。

一兩年前,或更早以前,Ajax風靡全球,曆時長久的BS/CS之争似乎可以蓋棺定論,當時我遇到的幾乎所有程式員都在孜孜不倦地談論着浏覽器上的那檔子事。時至今日,BS應用仍然比CS更能迎合程式員的口味。不過主子產品架構我依然選擇CS模式,理由我就不贅述了。什麼?非得給個說法?那我就陳列若幹理由如下:

  1. 浏覽器不是作業系統。微軟可以将HTML5和JS移入作業系統,卻不能将C#移入浏覽器。網際網路發展将出現越來越多的應用,總有一天臃腫的浏覽器會不堪重負,新的應用将隻能依靠更多樣的其它技術平台。你說Silverlight?這玩意我一直不看好,雖然我用WPF好久,雖然WPF程式轉成Silverlight應用号稱非常簡單,但是我從來沒去研究過Silverlight。Silverlight的前景也确實不甚光明。
  2. 随着網速的提高,我估計CS中Client的概念也将模糊。未來的應用對于用戶端來說,也許就是一個快捷圖示,而指向的位址是伺服器,應用程式不需要安裝,隻是在需要的時候實時下載下傳到用戶端。
  3. 上述兩條太空泛,也很容易被噴。如果站在客戶的角度,CS更有可能實作他們衆多的“無理要求”。對于服裝系統來說,BS适合資料展現(現在的上司都喜歡拿個IPAD在那算利潤,咱對IOS是外行,隻能從浏覽器上下手)。
  4. ……

我認為CS的缺點主要在于安裝和更新,前者隻能寄希望于上述第2條,咱們可以努力解決的就是版本更新。好的更新功能需要包含以下幾點:

  1. 版本釋出工具
  2. 線上自動更新
  3. 運作時手動更新(可選擇更新版本)
  4. 不同客戶不同版本
  5. 可以設定是否強制更新
  6. 版本清單檢視
  7. 版本還原(最多隻能還原至最近強制更新版本)
  8. 更新失敗後復原,并讓使用者選擇直接運作程式or重新更新
  9. 删除過期檔案

上述紅字表示暫時未開發。本着通用的原則,我建了幾個産品無關的類。

我的服裝DRP之線上更新

這幾個類簡單明了,無需多做解釋,我們需要的是一個工具來維護它們,下面是其中一個界面的截圖,由于開發倉促,該工具并不完善(我自己用用足矣)。

我的服裝DRP之線上更新

不完善的其中一個點我已經在圖中注明,該點産生的原因是由于待更新檔案清單支援檔案夾,如<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擷取版本資訊。代碼就不貼了,下面給個界面截圖。

我的服裝DRP之線上更新

改進點:需要在該界面上增加手動更新的按鈕,以及目前運作版本标示。

文至此,想到尚有一些業務需求未完成,再無下筆欲望,若有朋友感興趣,我會在空閑時間将該功能涉及到的幾個工具完善後剝離出來提供下載下傳。

轉載本文請注明出處:

http://www.cnblogs.com/newton/archive/2013/01/12/2857722.html