我的服装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. 删除过期文件
  10. ……

上述红字表示暂时未开发。本着通用的原则,我建了几个产品无关的类。

我的服装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

上一篇:前端开发框架Bootstrap和KnockoutJS


下一篇:业界首个AI云服务报告:阿里云进入领导者象限,力压亚马逊、微软