最近為客戶組織了一項(xiàng)C/S架構(gòu)程序的開發(fā)培訓(xùn),講解C/S應(yīng)用程序開發(fā)中需要注意的點(diǎn)。

我主要是做C/S方面的ERP/CRM程序開發(fā),界面是用Windows Forms技術(shù),有遺漏或錯(cuò)誤的地方歡迎批評(píng)指正。

1 異常處理

為處理應(yīng)用程序中的異常,需要增加以下代碼。

Application.ThreadException += new ThreadExceptionEventHandler(eh.OnThreadException);
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);


2  Excel文件生成

我們以Infragistics Excel作為生成Excel的基礎(chǔ)組件。它提供一套面向?qū)ο蟮哪P鸵院喕疎xel文件操作。

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;

如果需要將網(wǎng)格數(shù)據(jù)導(dǎo)出為Excel,它專門為此提供一個(gè)導(dǎo)入格式對(duì)象,簡單的調(diào)用以下代碼即可達(dá)到目的。

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 第三方類庫

為了簡化第三方類庫的部署,我在項(xiàng)目中直接將需要引用到的第三方類庫作為嵌入的資源生成為一個(gè)程序集。

這樣在部署時(shí),根據(jù)需要將我生成的程序集復(fù)制到執(zhí)行文件目錄即可。同時(shí)需要增加一個(gè)程序集加載事件。

AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;

static System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
     return EmbeddedAssembly.Get(args.Name);
}

這個(gè)技巧來自于CodeProject,參考以下地址Load DLL From Embedded Resource

 

4 日志追蹤

部署到生產(chǎn)環(huán)境中后,難免會(huì)出一些不可預(yù)料的異常。我使用SmartInspectPro來跟綜這些問題。

官方網(wǎng)址是 http://www.gurock.com/smartinspect/

只需要下面簡單的幾行代碼,就可以將程序中的異常信息或?qū)ο笮畔⑺鸭饋?,傳送到日志查看工具中?/p>

SiAuto.Si.Connections = "file(filename=c:\\log.sil)";
SiAuto.Si.Enabled = true;
SiAuto.Main.LogMessage("First Message!");

日志的內(nèi)容可以寫到文件,或是通過TCP或命名管道(named-pipes)發(fā)送到工具窗口中。

SiAuto.Si.Connections = string.Format("tcp(host={0},timeout=10000)", Microsoft.Common.Shared.ApplicationServer);

5 自動(dòng)更新

以文件所在的位置來區(qū)分,我們考慮局域網(wǎng),HTTP,F(xiàn)TP三種自動(dòng)更新方式。.NET有許多自動(dòng)更新組件,簡單的列舉。

 http://wyday.com/wyupdate/

序號(hào)名稱地址
1AutoUpdater.NEThttps://autoupdaterdotnet.codeplex.com/
2wyUpdatehttp://wyday.com/wyupdate/
3Updaterhttp://www.codeproject.com/Articles/9566/Updater
4NetSparklehttp://netsparkle.codeplex.com/
5NAppUpdatehttps://github.com/synhershko/NAppUpdate
6AutoUpdaterhttps://autoupdater.codeplex.com/

微軟本身也提供ClickOnce方式的更新方法,由于配置稍微麻煩我們并未采用。

6 版本檢測

由于有多個(gè)客戶的版本同時(shí)存在,我們?cè)谙到y(tǒng)啟動(dòng)時(shí),會(huì)檢測當(dāng)前文件夾中的所有文件的版本是否一致,如果不一致則拋出異常,終止執(zhí)行??蓞⒖既缦碌拇a片段。

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等源代碼管理工具,而是如何控制客戶正在使用的版本和程序員的開發(fā)版本。程序員的開發(fā)版本功能最多,同時(shí)也問題最多,許多新功能加入到程序中,沒有經(jīng)過完整的測試。

Team Foundation有一個(gè)分支管理功能,可以將客戶正在使用的版本(正式版)看作是開發(fā)版本的(程序員開發(fā))的一個(gè)子分支,每當(dāng)在開發(fā)版中check in某項(xiàng)bug fix或feature并且經(jīng)過完整測試后,將開發(fā)版本的變更集(changeset)合并到客戶正在使用的分支版本中。

 

8 x86 x64 Any CPU的選擇

現(xiàn)在.NET程序員真是太幸福了,編譯時(shí)設(shè)定為Any CPU,JIT運(yùn)行時(shí)根據(jù)機(jī)器的架構(gòu)(x86,x64)生成相應(yīng)的機(jī)器碼。

我們的項(xiàng)目絕大多數(shù)情況下都選Any CPU作為生成架構(gòu)。如果遇到一些編譯依賴項(xiàng)它只有x86版本的程序集,這時(shí)我們考慮將依賴于這個(gè)x86的程序集的功能單獨(dú)設(shè)計(jì)為一個(gè)DLL或EXE,這樣整個(gè)項(xiàng)目還是以Any CPU架構(gòu)來編譯。

有時(shí)候出于安全原因,有一些代碼以native語言來編寫,比如C++,這時(shí)我們就分別生成兩套(x86和x64)程序集,在部署時(shí)根據(jù)目標(biāo)平臺(tái)來部署相應(yīng)架構(gòu)的文件。


9 資源(圖片,文檔模板,標(biāo)準(zhǔn)報(bào)表)

為簡化部署,我們將常用的資源項(xiàng)編譯到一個(gè)程序集中??蓞⒖家韵麓a提取嵌入的資源項(xiàng)。

 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));
       }
   }
}

運(yùn)行時(shí)我們從程序集中提取資源到硬盤臨時(shí)文件夾,根據(jù)需要生成相應(yīng)的文件返回給用戶。

 

10 數(shù)據(jù)庫訪問

大型的項(xiàng)目離不開ORM,對(duì)象之間的運(yùn)算與關(guān)聯(lián)已不容易相處,如果還要去考慮數(shù)據(jù)讀寫,那程序的可維護(hù)性相對(duì)差很多。ORM帶來的好處除了數(shù)據(jù)讀寫的完全解放,還有強(qiáng)類型的數(shù)據(jù)綁定。為此,我們的數(shù)據(jù)讀寫接口都是用Code Smith模板生成的,比如一個(gè)對(duì)象的讀取方法

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帶來另一個(gè)好處是強(qiáng)類型綁定,這樣在設(shè)計(jì)時(shí)即可預(yù)知對(duì)象的類型和它的屬性成員,方便做數(shù)據(jù)綁定。

ORM的第三個(gè)好處,可能是勝于直接寫SQL語句(事務(wù)腳本模式)的地方,它會(huì)默認(rèn)檢測對(duì)象有哪些屬性發(fā)生值改變,這樣在保存對(duì)象時(shí)只會(huì)生成這些有發(fā)生值變更的SQL更新語句。許多同事甚至于我的上司都極度懷疑ORM的性能,我不確定他們是否真的驗(yàn)證過SQL語句(事務(wù)腳本模式)和ORM的性能比較。

11 性能

寫的不合理的代碼會(huì)導(dǎo)致性能問題,但不至于上升到懷疑技術(shù)的程度。微軟的Entity Framework有那么多客戶在用,難道這些客戶的程序都是小規(guī)模,小應(yīng)用嗎? .NET代碼的性能問題,我舉例以下幾個(gè)。

1) 主動(dòng)要求GC進(jìn)行垃圾回收會(huì)導(dǎo)致性能問題。

GC.Collect(GC.MaxGeneration, GCCollectionMode.Optimized);
GC.WaitForPendingFinalizers();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Optimized);
最后在stackoverflow中找到回答是,任何時(shí)候都不應(yīng)該調(diào)用此代碼,注釋以上代碼后程序速度是快很多了。

2)  釋放內(nèi)存的代碼會(huì)導(dǎo)致性能問題

[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) 反射會(huì)影響性能

這個(gè)結(jié)論不是空口而談,我是用ANTS Performance Profiler 8親自測試反射和非反射的代碼的運(yùn)行時(shí)間得出的結(jié)論。

比如我想增加一個(gè)動(dòng)態(tài)報(bào)表控件,根據(jù)系統(tǒng)安裝的水晶報(bào)表的版本來加載水晶報(bào)表控件。于是有以下兩種寫法

//反射版
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();

之后調(diào)用Load方法,反射版的Load方法需要耗費(fèi)的時(shí)間要比非反射版本多一倍左右。

ReflectionHelper.InvokeMethod(_crystalReportViewer, "Load", new System.Type[] {typeof (string), obj3.GetType()}, new object[] {path, obj3});

至于是否要用反射,我的結(jié)論是取決于應(yīng)用場景。如果應(yīng)用要求運(yùn)行速度第一,可維護(hù)性其次。則應(yīng)用最快的那種方法。比如有些醫(yī)藥行業(yè)的錄單模塊,對(duì)鍵盤的響應(yīng)速度要求極高,這時(shí)用反射是不合適的。

反射可以通過預(yù)處理(pre-init,pre-load)等方式提高響應(yīng)速度,這樣可在性能和可維護(hù)性方面雙贏。

4) 頻繁的數(shù)據(jù)庫讀寫會(huì)有性能問題

ORM實(shí)在是太方便了,各種計(jì)算和取值,只需要取到對(duì)象即可完成,代碼的可復(fù)用性高。不過有時(shí)候會(huì)導(dǎo)致性能問題。

在包含很多邏輯操作時(shí),為了取一個(gè)字段值而去頻繁的構(gòu)造對(duì)象是不合適的。比如在一個(gè)采購單列表功能中,為了取到采購單的部門編碼對(duì)應(yīng)的部門名稱,我們頻繁的去取數(shù)據(jù)庫,并且以構(gòu)造對(duì)象的方法來完成,這樣會(huì)導(dǎo)致性能問題。正確的做法是構(gòu)造DataTable來完成,構(gòu)造一個(gè)包含1000行記錄的DataTable要比構(gòu)造1000個(gè)部門對(duì)象(DepartmentEntity)要快很多。

ORM另一個(gè)好處是按需分配,我們可以根據(jù)需要只讀取部分字段的值,好比SELECT * 與SELECT 具體字段的區(qū)別。

參考以下的代碼,為了提高性能,我們的系統(tǒng)絕大多數(shù)情況下都是以這種方式讀取數(shù)據(jù)庫字段。

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 對(duì)象可以理解為SELECT語句中的具體字段的集合。

5) 控件的不合適操作會(huì)引起性能問題

設(shè)定選項(xiàng)卡控件的選中的方法,以下代碼中第一種要比第二種快

//快一點(diǎn)
tabControl.SelectedTab=tabControl.Tabs[0];
//慢一些
tabControl.Tabs[0].Selected=true;

水晶報(bào)表控件的設(shè)定數(shù)據(jù)源連接的時(shí)候,ApplyLogonInfo要比SetConnection慢。

//快一點(diǎn)的代碼
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程序包含豐富的事件機(jī)制,我認(rèn)為可用性要高于B/S程序。但是隨之而來的是代碼要比B/S慢。

當(dāng)我們的程序中有太多事件時(shí),我們需要在窗本釋放時(shí),將這些事件從委托鏈中移出。

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);
}
這個(gè)方法也是為了改善性能。
 

分類: 制造業(yè)信息化研究與開發(fā)