企業管理軟件包含一些公共的組件,這些基礎的組件在每個新項目立項階段就必須考慮。核心的穩定不變功能,方便系統開發與維護,也為系統二次開發提供了諸多便利。比如通用權限管理系統,通用附件管理,通用查詢等組件,若是在項目開發前就准備好了這些組件,為項目如期交付提供了保證。
- 查詢設計器 Query Designer 支持選擇一個或多個數據庫表,通過左右連接的方式構建查詢結果,支持直接手寫SQL語句設計查詢,支持調用存儲過程查詢,支持用代碼設計查詢。
- 報表設計器 Report Designer 支持配置的方式生成報表參數對話框,支持報表多語言,支持報表參數設置與打印
- 窗體設計器 Form Designer 支持用戶自定義界面控件的布局(Layout)和外觀(Appearance),支持用戶修改界面控件的查找條件
- 工作流設計器 Workflow Designer 支持自定義流程,支持自定義消息通知,支持自定義事件,支持計划任務
- 任務計划設計器 Schedule Designer 支持計划任務
查詢設計器 Query Designer
在系統維護過程中,不停的增加新的字段,一般不會立即重寫系統現有的查詢,但需要一種方式可以立即看到系統的更改,或者是系統現有的查詢不能滿足客戶的需求,查詢設計器就是為了解決系統查詢功能的不足開發的。
我考慮到了以下幾種查詢的方式,簡單介紹它們的實現方式供參考。
1 SQL語句 SQL Statements
假設客戶的系統維護人員懂SQL語句,經過多年客戶積累的系統,軟件公司也有許多常用的查詢語句,將這些查詢語句放到一個查詢功能中執行一下,即可獲取數據。
參考下面的SqlDbx程序的例子,在系統中可以對這個界面進行簡化,只保留輸入SQL語句和顯示查詢結果的地方。
為了方便最終用戶分析結果數據,系統需要提供對查詢結果數據的導出(Microsoft Excel),過濾,排序,分組等功能。
ERP系統將每個查詢保存起來,下次用戶只需要敲查詢編號即可看到查詢結果,並對結果數據進行操作。
2 圖形化查詢設計 GUI Query Design
如果用戶不懂SQL語句,系統考慮提供一種圖形化的方式供用戶設計查詢。記得10年前自己在學習SQL語句的時候,是非常期待有一個智能化的工具,可以讓我選擇要查詢的數據表和字段,再設置數據表之間關聯,最后就得到我需用的查詢語句。系統參考了Access 的查詢設計器,參考一下經典的Access的查詢設計界面:
微軟Office套件中Access的查詢設計器經過多年的發展,應該是有足夠的理由相信這個界面是最容易讓非IT人士接受的查詢設計界面。只需要用鼠標選一下要查詢的表,再選擇要顯示的字段,系統自動產生相應的SQL語句。
圖紙化查詢設計這種功能會經常出現在報表設計器中,報表設計器一般都會附加一個圖形化的查詢設計工具,我們可以在那里找到它的界面原型,經過簡化后變成ERP系統的查詢設計工具。
3 存儲過程查詢 Query By Stored Procedure
如果SQL語句或是表關聯也不能滿足數據的查詢要求,系統可以考慮增加存儲過程支持,以滿足更復雜的查詢需求。在ERP的財務報表中,各種財務統計報表的確相當的繁瑣,非用存儲過程不可。我們需要考慮好,如何將參數傳遞到存儲過程中,顯示存儲過程的返回結果就可滿足這種需求。為此,設計如下的查詢語句:
EXEC spRpt_OrderAmtTotal %1, %2
存儲過程的后面兩個參數是占位符號,表示需要給此存儲過程傳遞參數。於是,還需要設計一種參數映射,設定參數的類型,長度,運行此查詢時,將用戶輸入的值替換到上面的兩個占位置符中,傳遞到存儲過程中。
存儲過程的定義已經定義它的參數類型,系統運行查詢時,系統需要將上面的%1的值所代表的值轉換成存儲過程需要的參數類型,比如上圖中的%1 所代表的Date From,系統需要用戶的輸入值進行強制類型轉換為日期時間類型,再傳遞到存儲過程中獲取返回結果。
4 程序查詢 Query By Program
用程序代碼寫查詢可以分為二種情況,單據查詢,列表查詢。單據查詢只需要繼承原有的單據編輯窗體,設置窗體不可編輯,這樣就完成了單據查詢。列表查詢是指需要用戶輸入一種或幾種過濾條件,根據過濾條件得到查詢結果。
單據查詢的代碼非常簡單,只是重設幾個屬性即可,參考下面的代碼例子。
public partial class SalesContractEnquiry : Foundation.Sales.Entry.SalesContractEntry { public CostSheetEnquiry() { InitializeComponent(); this.SupportAdd = false; this.SupportDelete = false; this.SupportEdit = false; }
程序代碼中禁用了單據的新增,刪除,編輯操作,這樣單據窗體變也了查詢窗體。
列表查詢的界面參考如下,界面中上方是過濾條件輸入控件,下面顯示查詢結果。對查詢結果可以導出Excel,過濾,分組,或是以圖表的方式呈現查詢結果。
報表設計器 Report Designer
報表設計器分二個組件,一個是報表設計,另一個是報表顯示,前者是design,后者是render。市面上有許多報表設計工具,我比較熟悉是的水晶報表(Crystal Report)和報表服務(SQL Server Reporting Services)。剛畢業那時還接觸到開源的報表設計器RDLC Designer,是微軟報表服務的一個開源實現。工作中接觸水晶報表的時間比較多,我的技術總監寫的一個水晶報表查看器,全是反射調用做成的報表查看器,不依賴於水晶報表的版本,發現水晶報表對.NET的支持相當穩定,從Crystal Report for Visual Studio 2008 runtime到現在的Crystal Report 13.10,幾乎沒有改動代碼就可以完美的運行技術總監的代碼,水晶報表是.NET報表制作的工業標准。
能力有限,實在沒有精力去維護一份報表設計器代碼,報表設計選用SAP的水晶報表設計器,這個工具有很多年沒有更新,目前能找到的最新版本是Crystal Report 200 SP2。
所以這一部分的內容測重於報表呈現(Render),力求設計一個完美的報表閱讀器。作為核心功能,列出如下需求:
- 報表設計完成后,不需要.NET編程即可調用報表,主要的困難在於參數傳遞。
- 報表支持多語言,也就是支持英語,簡體,繁體三種語言顯示報表。
- 報表部署方便,只需要安裝一個Crystal Report runtime就可以運行報表。
- 報表定制容易,用戶既可以用系統提供的標准報表,也可用定制修改報表。
1 參數傳遞 Dynamic Parameter
水晶報表分三種類型的參數,通過調節這三個數值來改變水晶報表的數據。定義以下枚舉:
public enum ReportFieldType { [DisplayText("Selection Formula"), StringValue("0")] SelectionFormula = 0, [DisplayText("Formula Field"), StringValue("1") ] FormulaField = 1, [DisplayText("Parameter"), StringValue("2")] Parameter = 2 }
以上三種種方式的定義,與下面的水晶報表截圖可以更清楚的了解它們的含義:
0 表示表記錄選擇條件,1 表示公式,2表示參數。 通過這三種方式,可以定義如下表結構:
運行報表時,先根據上面的參數表生成控件,獲取控件的值,傳遞到水晶報表中,即完成了參數傳遞。
這樣開發的好處是技術支持人員可獨立制作和開發報表,不需要開發部專門為每個界面開發參數選擇界面。
2 多語言 Localization
如何只設計一份報表,卻可以讓它同時以三種本地化語言顯示報表。經歷了以下幾種方案演化。
1) 定義一個LanguageCode的公式或參數,運行時由系統傳入到報表中來,表示當前要顯示的語言。水晶報表中每個文字標簽都用公式表示,公式Ccy的例子參考如下:
if LanguageCode=1 then "Currency" else if LanguageCode=2 then “貨幣” else "貨幣"
從公式中可以看到,1表示英語,2表示繁體中文,其它的數字表示簡體中文。這樣根據傳入的LanguageCode的值,來顯示文本標簽的值,實現了報表多語言顯示。
2) 使用.NET Localization方案,定義三種資源文件,分別是Resource.en-us.resx,Resource.zh-cn.resx,Resource.zh-tw.resx,編譯這個程序集后會生成三個帶語言標識的子程序集,.NET運行時會根據語言查找相應的資源文件,調用語言配對的資源。關鍵的代碼調用如下所示:
System.Threading.Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(cultureName);
System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo(cultureName);
3) 翻譯水晶報表控件TextObject
這個方法是借助於水晶報表.NET API,找到水晶報表中需要翻譯的對象,一般是TextObject,將它翻譯成對應的本地化語言再顯示,這種方案深受報表開發人員喜愛,開發報表時只需要按照標准英文版的做法,當需要顯示為其它語言時,自動轉化為本地化語言。可參考下面的代碼片段以加深了解。
IEnumerator reportObjectEnumerator = (IEnumerator)ReflectionHelper.InvokeMethod(reportObjects, "GetEnumerator"); while (reportObjectEnumerator.MoveNext()) { try { object reportObject = reportObjectEnumerator.Current; object objectKind = ReflectionHelper.GetPropertyValue(reportObject, "Kind"); string objectKindName = Enum.GetName(objectKind.GetType(), objectKind); if (string.CompareOrdinal(objectKindName, "TextObject") == 0 || string.CompareOrdinal(objectKindName, "FieldHeadingObject") == 0) { string text = (string)ReflectionHelper.GetPropertyValue(reportObject, "Text"); if (!string.IsNullOrEmpty(text)) { string translatedText = ComponentCommon.TranslateText(text, false); if (string.CompareOrdinal(text, translatedText) != 0) ReflectionHelper.SetPropertyValue(reportObject, "Text", translatedText); } } } catch { } }
反射調用一個對象要實現foreach的效果,需要調用GetEnumerator方法。
3 部署 Deployment
寫一個水晶報表運行庫的檢測程序,它可以檢測安裝的水晶報表的版本。水晶報表控件全部用反射方法調用,參考下面的代碼例子,這樣就實現了編譯時不依賴於水晶報表版本的效果,部署時更加靈活方便。
ReflectionHelper.SetPropertyValue(this._crystalReportViewer, "ReportSource", this._report); ReflectionHelper.InvokeMethod(this._crystalReportViewer, "Update"); ReflectionHelper.InvokeMethod(this._crystalReportViewer, "Zoom", new System.Type[] { typeof(int) }, new object[] { 1 }); ReflectionHelper.InvokeMethod(this._crystalReportViewer, "Zoom", new System.Type[] { typeof(int) }, new object[] { this._zoomFactor });
4 報表定制
引入一個替代報表(Alternate report)的概念,將標准報表嵌入到程序集或放置在標准報表目錄中,系統也支持一個替代報表的路徑選項,系統讀取報表時優先查找替代報表路徑中的報表文件,找到則用替代報表顯示,否則繼續在標准報表路徑中查找報表。因為兩種報表放置在不同的路徑中,所以相同的報表文件也不會相互影響和覆蓋,解決了客戶定制報表與系統報表取舍的難題。替代(Alternate)的概念還用在物料清單的替代物料中,生產發料時當物料不夠發料,可以去查找替代物料發料,好比我們口渴了可以喝汽水,也可以選擇喝涼茶。
窗體設計器 Form Designer
窗體設計器在ERP/MIS領域應用的相當廣泛,Visual Studio本身就是一個設計精良的窗體設計器。金蝶ERP的BOS系統全部依賴於它的窗體設計器,在此基礎上做數據綁定和插件開發。微軟的InfoPath也是一個自定義表單工作,常用來做OA系統的自定義表單。剛畢業那會也非常喜歡研究form designer re-host技術,可惜一直沒有找到技術突破點,也不知道這樣的設計是否合理。曾經接觸過《像搭積木一樣做軟件》這本書,全書講解的就是以窗體設計器為基礎,做表達式求值,做事件綁定和屬性綁定,不需要編碼而開發企業管理軟件。
然而這種美好的技術終究是一種幻覺,Visual Studio 仍舊是最流行的開發工具,窗體開發仍舊是企業管理軟件開發的重點。窗體設計器所產生的代碼,只有一小部分間接的用途。我沒有深入接觸這個主題,但就我所知道的知識點列舉如下。
1 窗體設計器可以生成CS/VB/Xml 三種代碼格式。NET動態編譯技術已經很成熟,直接調用.NET API就可以將CS/VB代碼編譯為程序集,在這里我選擇第三種格式,我並不需要用窗體設計器完全開發一個新功能,那樣涉及到數據綁定,主從數據等一系列難題,我只需要設計器產生Xml格式,運行時我可以優先加載這個自定義布局,所以Xml格式足矣。
2 要設計的窗體對象是現有的系統功能。用戶可能要修改界面控件的布局或是外觀。實施過程中,看到很多客戶喜歡將控件標成紅色或藍色以加快閱讀速度,然而當滿屏幕都是花花綠綠的控件,反而會降低閱讀理解的速度。另一個就是控件的布局,一些用戶不需要的選項卡,控件可以通過窗體設計器隱藏。
3 窗體設計器最重要的地方是可以修改界面控件的查找。參考下圖。
窗體設計器可以修改Customer No中查找按鈕的過濾條件,這一重要的功能大大減輕了開發人員的負擔。
4 窗體設計器不可以用來完全重新開發一個新功能,從界面設計到數據綁定,再到數據驗證,以及數據之間的勾稽
引用,這些功能的實現不可能通過鼠標拖放控件就完成。即使通過大量的二次開發,像金蝶那樣做成BOS,它的可擴展性和靈活性仍那以控制。比如單價 * 數量=金額,要做到輸入單價或數量時,自動計算金額。BOS要考慮的內容項太多,我終究是徹底放棄這種開發模式,只用到窗體設計器的一小部分功能:控件外觀與布局修改,控件查找定制。
工作流設計器 Workflow Designer
工作流實現的四大基礎功能:通知提醒,批核,計划任務,調用自定義代碼。
為了實現這個目標,基於微軟的.NET WF,做了以下工作以達到上述目的。
1 定義活動庫 Activities
活動是工作流中的代碼執行單元,一個工作流定義本身也是一個活動。根據業務需要,定義了如下活動庫:
文檔批核活動,發送消息活動,發送郵件活動,調用.NET 代碼活動,執行數據庫查詢活動,報表生成活動。
2 定義工作流類型 Workflow
根據業務的需要,定義以下幾種業務類型:
單據類:單據保存,單據更新,單據刪除,單據新建。
業務類:文檔送審,文檔批核,業務過帳,業務完成,業務取消。
任務計划:在預定時間執行工作流
3 工作流設計器 Workflow Designer Rehost
MSDN 中提供了rehost工作流設計器的例子,直接把它拿來參考,添加自定義活動組件和自定義工作流類型,再將工作流設計器輸出格式保存為XOML,即可完成工作流設計器的絕大部分功能。
這里比較復雜的一點是自定義條件表達式,需要一個與對象表達式求值工具。Code Project中有許多條件表達式的例子,難點在於如何將業務單據的狀態與工作流掛接。
4 工作流監控器 Workflow Monitor
系統需要一個可視化的工具查看每個流程當前正在運行的結點,MSDN中有例子可參考。
5 工作流持久化 Workfllow Persistence
.NET提供的基礎服務,創建一個工作流狀態保存數據庫和一個工作流跟蹤數據庫。
6 工作流服務器 Workflow Server
工作流與系統業務部分的交互,專門開一個獨立的端口用於數據的讀寫。
任務計划設計器 Schedule Designer
企業應用中的作業調度,常見的操作如下:郵件提醒和告警,執行文件傳輸操作,創建復雜報表。
系統分為兩種計划任務調度器,一種是基於SQL Server Agent實現,定時執行SQL語句,另一種是基於Quartz框架庫實現,用.NET代碼開發任務調度程序。
1 基於SQL Server Agent
SQL Server Job是一個定期執行腳本的對象,給它加一個時間選項即可完成基於SQL Server Agent的計划調度程序。
這個界面會依據控件值的不同,生成不同參數的SQL Server Job,參考下面的程序代碼片段:
private void btnOk_Click(object sender, EventArgs e) { if (Schedule == null) Schedule = new SQLschedule(); Schedule.name = txtName.Text.Trim(); if (chkEnabled.Checked) Schedule.enabled = 1; else Schedule.enabled = 0; if (cmbScheduleType.SelectedIndex == 1) { Schedule.freq_type = 1; // One-time Schedule.active_start_date = dtOneTimeOccurDate.Value; Schedule.active_end_date = new DateTime(9999, 12, 31); Schedule.active_start_time = dtOneTimeOccurTime.Value; Schedule.active_end_time = new DateTime(2000, 1, 1, 23, 59, 59); } else { if (cmbFrequencyOccurs.SelectedIndex == 0) { Schedule.freq_type = 4; // Daily Schedule.freq_interval = (int) numFrequencyRecurs.Value; } if (cmbFrequencyOccurs.SelectedIndex == 1) { Schedule.freq_type = 8; // Weekly int freq_interval = 0; if (chkFrequencyRecursSun.Checked) freq_interval += 1; if (chkFrequencyRecursMon.Checked) freq_interval += 2; if (chkFrequencyRecursTue.Checked) freq_interval += 4;
這個工具來源於Code Project上的一篇文章,可以用SQL Agent Job Editor嘗試找到它。
2 Quartz.NET 任務調度框架
這是由Java項目轉化過來的著名項目,用.NET代碼重定了它的邏輯。看一個最簡單的任務計划的源代碼。
public class HelloJob : IJob { private static ILog _log = LogManager.GetLogger(typeof(HelloJob)); public HelloJob() { } public virtual void Execute(IJobExecutionContext context) { // Say Hello to the World and display the date/time _log.Info(string.Format("Hello World! - {0}", System.DateTime.Now.ToString("r"))); } }
Quartz.NET處理好了關於任務計划調度方面的各個細節,很容易上手,官方提供的例子也相當豐富。