最近为客户组织了一项C/S架构程序的开发培训,讲解C/S应用程序开发中需要注意的点。
我主要是做C/S方面的ERP/CRM程序开发,界面是用Windows Forms技术,有遗漏或错误的地方欢迎批评指正。
1 异常处理
为处理应用程序中的异常,需要增加以下代码。
Application.ThreadException += new ThreadExceptionEventHandler(eh.OnThreadException);
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
2 Excel文件生成
我们以Infragistics Excel作为生成Excel的基础组件。它提供一套面向对象的模型以简化Exel文件操作。
excelWorkbook = new Workbook();
Worksheet currentWorksheet = this.excelWorkbook.Worksheets.Add("WorkSheet1");
foreach (var cell in currentWorksheet.GetRegion("A1:D1"))
{
cell.CellFormat.Fill = CellFill.CreateSolidFill(Color.Gray);
cell.CellFormat.Font.ColorInfo = new WorkbookColorInfo(Color.White);
}
currentWorksheet.Rows[0].Cells[0].Value = "Order ID";
currentWorksheet.Rows[0].Cells[1].Value = "Contact Name";
currentWorksheet.Rows[0].Cells[2].Value = "Shipping Address";
currentWorksheet.Rows[0].Cells[3].Value = "Order Date";
currentWorksheet.Columns[0].Width = 3000;
currentWorksheet.Columns[0].CellFormat.Alignment = HorizontalCellAlignment.Left;
currentWorksheet.Columns[1].Width = 7100;
currentWorksheet.Columns[2].Width = 3000;
currentWorksheet.Columns[2].CellFormat.Alignment = HorizontalCellAlignment.Left;
currentWorksheet.Columns[3].Width = 6100;
如果需要将网格数据导出为Excel,它专门为此提供一个导入格式对象,简单的调用以下代码即可达到目的。
using (System.Windows.Forms.SaveFileDialog dialog = new System.Windows.Forms.SaveFileDialog())
{
dialog.DefaultExt = "xls";
dialog.Filter = Shared.ExportToFileFilter;
dialog.Title = Microsoft.Common.Shared.TranslateText("Export to File");
dialog.FileName = this.Text;
if (dialog.ShowDialog() != DialogResult.OK)
{
return;
}
if (dialog.FilterIndex == 1 || dialog.FilterIndex == 2)
{
using (UltraGridExcelExporter exporter = new UltraGridExcelExporter())
{
exporter.BandSpacing = BandSpacing.None;
exporter.Export(gridFunction, dialog.FileName);
}
}
}
3 第三方类库
为了简化第三方类库的部署,我在项目中直接将需要引用到的第三方类库作为嵌入的资源生成为一个程序集。
这样在部署时,根据需要将我生成的程序集复制到执行文件目录即可。同时需要增加一个程序集加载事件。
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
static System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
return EmbeddedAssembly.Get(args.Name);
}
这个技巧来自于CodeProject,参考以下地址Load DLL From Embedded Resource
4 日志追踪
部署到生产环境中后,难免会出一些不可预料的异常。我使用SmartInspectPro来跟综这些问题。
官方网址是 http://www.gurock.com/smartinspect/
只需要下面简单的几行代码,就可以将程序中的异常信息或对象信息搜集起来,传送到日志查看工具中。
SiAuto.Si.Connections = "file(filename=c:\log.sil)";
SiAuto.Si.Enabled = true;
SiAuto.Main.LogMessage("First Message!");
日志的内容可以写到文件,或是通过TCP或命名管道(named-pipes)发送到工具窗口中。
SiAuto.Si.Connections = string.Format("tcp(host={0},timeout=10000)", Microsoft.Common.Shared.ApplicationServer);
5 自动更新
以文件所在的位置来区分,我们考虑局域网,HTTP,FTP三种自动更新方式。.NET有许多自动更新组件,简单的列举。
http://wyday.com/wyupdate/
序号 | 名称 | 地址 |
1 | AutoUpdater.NET | https://autoupdaterdotnet.codeplex.com/ |
2 | wyUpdate | http://wyday.com/wyupdate/ |
3 | Updater | http://www.codeproject.com/Articles/9566/Updater |
4 | NetSparkle | http://netsparkle.codeplex.com/ |
5 | NAppUpdate | https://github.com/synhershko/NAppUpdate |
6 | AutoUpdater | https://autoupdater.codeplex.com/ |
微软本身也提供ClickOnce方式的更新方法,由于配置稍微麻烦我们并未采用。
6 版本检测
由于有多个客户的版本同时存在,我们在系统启动时,会检测当前文件夹中的所有文件的版本是否一致,如果不一致则抛出异常,终止执行。可参考如下的代码片段。
private static void VerifyAssembliesVersion()
{
string[] files = Directory.GetFiles(Application.StartupPath, "Microsoft.EnterpriseSolution.*.dll", SearchOption.TopDirectoryOnly);
Parallel.ForEach<string>(files, file =>
{
FileVersionInfo fileVersion = FileVersionInfo.GetVersionInfo(file);
if (string.CompareOrdinal(fileVersion.FileVersion, AssemblyVersion.FileVersion) != 0)
throw new AppException(string.Format("File version mismatch detected"); }
}
}
7 源代码控制
我要提到的不是Team Foundation,SVN或Visual SourceSafe等源代码管理工具,而是如何控制客户正在使用的版本和程序员的开发版本。程序员的开发版本功能最多,同时也问题最多,许多新功能加入到程序中,没有经过完整的测试。
Team Foundation有一个分支管理功能,可以将客户正在使用的版本(正式版)看作是开发版本的(程序员开发)的一个子分支,每当在开发版中check in某项bug fix或feature并且经过完整测试后,将开发版本的变更集(changeset)合并到客户正在使用的分支版本中。
8 x86 x64 Any CPU的选择
现在.NET程序员真是太幸福了,编译时设定为Any CPU,JIT运行时根据机器的架构(x86,x64)生成相应的机器码。
我们的项目绝大多数情况下都选Any CPU作为生成架构。如果遇到一些编译依赖项它只有x86版本的程序集,这时我们考虑将依赖于这个x86的程序集的功能单独设计为一个DLL或EXE,这样整个项目还是以Any CPU架构来编译。
有时候出于安全原因,有一些代码以native语言来编写,比如C++,这时我们就分别生成两套(x86和x64)程序集,在部署时根据目标平台来部署相应架构的文件。
9 资源(图片,文档模板,标准报表)
为简化部署,我们将常用的资源项编译到一个程序集中。可参考以下代码提取嵌入的资源项。
private static void ExtractEmbeddedResource(string resourceLocation, string output)
{
using (System.IO.Stream stream = Assembly.Load("Microsoft.Data").GetManifestResourceStream(resourceLocation))
{
using (BinaryReader r = new BinaryReader(stream))
using (FileStream fs = new FileStream(output, FileMode.OpenOrCreate))
using (BinaryWriter w = new BinaryWriter(fs))
{
w.Write(r.ReadBytes((int)stream.Length));
}
}
}
运行时我们从程序集中提取资源到硬盘临时文件夹,根据需要生成相应的文件返回给用户。
10 数据库访问
大型的项目离不开ORM,对象之间的运算与关联已不容易相处,如果还要去考虑数据读写,那程序的可维护性相对差很多。ORM带来的好处除了数据读写的完全解放,还有强类型的数据绑定。为此,我们的数据读写接口都是用Code Smith模板生成的,比如一个对象的读取方法
AccountEntity account = null;
using (DataAccessAdapter adapter = GetCompanyDataAccessAdapter(sessionId, companyCode))
{
account = new AccountEntity(accountNo);
bool found = adapter.FetchEntity(account, prefetchPath, null, fieldList);
if (!found) throw new RecordNotFoundException(accountNo, "Invalid Account No.");
}
ORM带来另一个好处是强类型绑定,这样在设计时即可预知对象的类型和它的属性成员,方便做数据绑定。
ORM的第三个好处,可能是胜于直接写SQL语句(事务脚本模式)的地方,它会默认检测对象有哪些属性发生值改变,这样在保存对象时只会生成这些有发生值变更的SQL更新语句。许多同事甚至于我的上司都极度怀疑ORM的性能,我不确定他们是否真的验证过SQL语句(事务脚本模式)和ORM的性能比较。
11 性能
写的不合理的代码会导致性能问题,但不至于上升到怀疑技术的程度。微软的Entity Framework有那么多客户在用,难道这些客户的程序都是小规模,小应用吗? .NET代码的性能问题,我举例以下几个。
1) 主动要求GC进行垃圾回收会导致性能问题。
GC.Collect(GC.MaxGeneration, GCCollectionMode.Optimized);
GC.WaitForPendingFinalizers();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Optimized);
最后在stackoverflow中找到回答是,任何时候都不应该调用此代码,注释以上代码后程序速度是快很多了。
2) 释放内存的代码会导致性能问题
[DllImport("kernel32.dll")]
private static extern bool SetProcessWorkingSetSize(IntPtr process, int minSize, int maxSize);
具体原因可参考这里
http://www.cnblogs.com/kex1n/archive/2011/01/26/2286427.html
3) 反射会影响性能
这个结论不是空口而谈,我是用ANTS Performance Profiler 8亲自测试反射和非反射的代码的运行时间得出的结论。
比如我想增加一个动态报表控件,根据系统安装的水晶报表的版本来加载水晶报表控件。于是有以下两种写法
//反射版
object _crystalReportViewer;
_crystalReportViewer = ReflectionHelper.CreateObjectInstance(CrystalReportHelper.GetLongAssemblyName("CrystalDecisions.Windows.Forms", CrystalReportVersion), "CrystalDecisions.Windows.Forms.CrystalReportViewer");
//非反射版
CrystalDecisions.Windows.Forms.CrystalReportViewer _crystalReportViewer;
_crystalReportViewer=new CrystalDecisions.Windows.Forms.CrystalReportViewer();
之后调用Load方法,反射版的Load方法需要耗费的时间要比非反射版本多一倍左右。
ReflectionHelper.InvokeMethod(_crystalReportViewer, "Load", new System.Type[] {typeof (string), obj3.GetType()}, new object[] {path, obj3});
至于是否要用反射,我的结论是取决于应用场景。如果应用要求运行速度第一,可维护性其次。则应用最快的那种方法。比如有些医药行业的录单模块,对键盘的响应速度要求极高,这时用反射是不合适的。
反射可以通过预处理(pre-init,pre-load)等方式提高响应速度,这样可在性能和可维护性方面双赢。
4) 频繁的数据库读写会有性能问题
ORM实在是太方便了,各种计算和取值,只需要取到对象即可完成,代码的可复用性高。不过有时候会导致性能问题。
在包含很多逻辑操作时,为了取一个字段值而去频繁的构造对象是不合适的。比如在一个采购单列表功能中,为了取到采购单的部门编码对应的部门名称,我们频繁的去取数据库,并且以构造对象的方法来完成,这样会导致性能问题。正确的做法是构造DataTable来完成,构造一个包含1000行记录的DataTable要比构造1000个部门对象(DepartmentEntity)要快很多。
ORM另一个好处是按需分配,我们可以根据需要只读取部分字段的值,好比SELECT * 与SELECT 具体字段的区别。
参考以下的代码,为了提高性能,我们的系统绝大多数情况下都是以这种方式读取数据库字段。
IItemManager itemMan = ClientProxyFactory.CreateProxyInstance<IItemManager>();
ExcludeIncludeFieldsList fieldList = new ExcludeIncludeFieldsList(false);
fieldList.Add(ItemFields.Description);
fieldList.Add(ItemFields.StockUom);
fieldList.Add(ItemFields.ScrapRate);
fieldList.Add(ItemFields.DefBomNo);
fieldList.Add(ItemFields.ExtendedDesc);
fieldList.Add(ItemFields.RohsCompliance);
fieldList.Add(ItemFields.TempDescription);
fieldList.Add(ItemFields.Specification);
fieldList.Add(ItemFields.ColorCode);
ItemEntity item = itemMan.GetValidItem(Shared.CurrentUserSessionId, this.PartItemNo, null, fieldList, Shared.SystemParameter.TailorSinojoint);
ExcludeIncludeFieldsList 对象可以理解为SELECT语句中的具体字段的集合。
5) 控件的不合适操作会引起性能问题
设定选项卡控件的选中的方法,以下代码中第一种要比第二种快
//快一点
tabControl.SelectedTab=tabControl.Tabs[0];
//慢一些
tabControl.Tabs[0].Selected=true;
水晶报表控件的设定数据源连接的时候,ApplyLogonInfo要比SetConnection慢。
//快一点的代码
reportDocument.DataSourceConnections[0].SetConnection(
connectionStringBuilder.DataSource,
connectionStringBuilder.InitialCatalog,
connectionStringBuilder.UserID,
connectionStringBuilder.Password
);
//慢一些的代码
crDatabase = crReportDocument,Database
crTables = crDatabase.Tables
For Each crTable In crTables
crTableLogOnInfo = crTable.LogOnInfo
crTableLogOnInfo.ConnectionInfo = crConnectionInfo
crTable.ApplyLogOnInfo(crTableLogOnInfo)
Next
12 事件销毁
C/S程序包含丰富的事件机制,我认为可用性要高于B/S程序。但是随之而来的是代码要比B/S慢。
当我们的程序中有太多事件时,我们需要在窗本释放时,将这些事件从委托链中移出。
protected override void ReleaseResources()
{
this.btnPrintRouting.Click -= new System.EventHandler(this.btnPrintRouting_Click);
this.btnPrintMaterialsList.Click -= new System.EventHandler(this.btnPrintMaterialsList_Click);
this.btnSortMaterials.Click -= new System.EventHandler(this.btnSortMaterials_Click);
}
protected override void Dispose(bool disposing)
{
if (disposing && components != null)
{
components.Dispose();
}
ReleaseResources();
base.Dispose(disposing);
}
这个方法也是为了改善性能。