最近為客戶組織了一項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);
}
這個方法也是為了改善性能。