問題篇:
昨天在CSDN看到這樣一個帖子:“苦逼的三層代碼”:
采用傳統的三層架構寫代碼,每個數據表都要定義一個實體對象,編寫后台的時候,
Web層需要針對頁面的用戶輸入逐個手動編寫賦值到實體對象的各個屬性,然后DAL層還要用SqlHelper 進行各個存儲過程對應參數的實體賦值,
我的天呀,寫幾個表還好,多個表呢,
寫的后台都沒力氣,
典型的苦逼代碼工沒營養,各位有啥好的處理方法或開發方式。。
看到跟帖,大部分都說使用ORM解決這個問題,但我覺得ORM還是沒有解決貼主的幾個問題:
- 每個數據表都要定義一個實體對象
- 頁面的用戶輸入逐個手動編寫賦值到實體對象的各個屬性
- 表很多,代碼重復量大,典型的苦逼代碼工
另外跟帖中也有不少上用動軟的三層代碼生成器,這個方法看似能夠解決一部分問題,但必須使用代碼生成器規定的那種三層結構,不利於靈活擴展,而且遇到業務稍復雜的情況,也不是代碼生成器能夠解決的問題。
實際上,對於問題1,問題2,我們按照一定規則,使用反射是可以解決對象屬性手工逐個賦值、取值的過程的,需要我們自己好好制定這個規則。這里我采用另外一種方案,不使用反射,“一行代碼”實現Web、WinForm窗體表單數據的填充、收集、清除,和到數據庫的CRUD,而秘訣就是對表單控件進行擴展。
原理篇:
我們常用的表單控件主要有以下幾個:
- CheckBox、
- DropDownList、
- Label、
- ListBox、
- RadioButton、
- TextBox
我們對這些控件進行擴展,讓它統一繼承一個數據接口 IDataControl:

/// <summary> /// 數據映射控件接口 /// </summary> public interface IDataControl { /// <summary> /// 與數據庫數據項相關聯的數據 /// </summary> string LinkProperty { get; set; } /// <summary> /// 與數據關聯的表名 /// </summary> string LinkObject { get; set; } /// <summary> /// 是否通過服務器驗證默認為true /// </summary> bool IsValid { get; } /// <summary> /// 數據類型 /// </summary> TypeCode SysTypeCode { get; set; } /// <summary> /// 只讀標記 /// </summary> bool ReadOnly { get; set; } /// <summary> /// 是否允許空值 /// </summary> bool isNull { get; } /// <summary> /// 是否是主鍵 /// </summary> bool PrimaryKey { get; set; } /// <summary> /// 設置值 /// </summary> /// <param name="obj"></param> void SetValue(object value); /// <summary> /// 獲取值 /// </summary> /// <returns></returns> object GetValue(); /// <summary> /// 服務端驗證 /// </summary> /// <returns></returns> bool Validate(); }
稍后我們來講這個接口的具體應用,下面,我們定義幾個新的數據控件,來繼承這個接口:
注:下面以WinForm控件為例子,WebForm與之類似。
public partial class DataCalendar : DateTimePicker, IDataControl { //數據日歷控件 } public partial class DataCheckBox : CheckBox, IDataControl { //數據復選框控件 } public partial class DataDropDownList : ComboBox, IDataControl { //數據下拉選擇框控件 } public class DataLabel : Label, IDataControl { //數據標簽控件 } public partial class DataListBox : ListBox, IDataControl { //數據列表框控件 } public partial class DataRadioButton : RadioButton, IDataControl { //數據選項按鈕控件 } public class DataTextBox : TextBox, IDataTextBox { //數據文本框控件 }
有了這些擴展的表單控件,我們只需要調用它的接口方法,進行賦值和取值:
DataTextBox dtb=new DataTextBox(); dtb.SetValue("text1"); string value=dtb.GetValue().ToString();//text1
而在DataTextBox的這兩個接口方法實現中,是不需要使用反射的:
public void SetValue(object obj) { if (obj == null || obj.ToString() == "") { this.Text = ""; return; } //其它檢測和格式控制代碼略 this.Text = obj.ToString().Trim(); } public object GetValue() { //其它檢測和格式控制代碼略 return this.Text.Trim(); }
有了數據控件的這2個接口方法,我們對各種數據控件進行統一的數據收集、填充就很容易了,無非就是遍歷一下窗體上面的數據控件,找到它們然后一個個處理即可,具體代碼后面的實例會說到。
既然說到表單數據的填充,將查詢出來的數據集中哪個表的某個字段和哪個控件對應呢?
這就用到了IDataControl接口的下面2個屬性了:
string LinkProperty{get;set;}//對應字段名或者實體類的屬性名 string LinkObject{get;set;}//對應表名或者實體類的類名稱
OK,有了IDataControl接口的這幾個接口方法和屬性,不使用反射,封裝一下,“一行代碼”實現Web、WinForm窗體表單數據的填充、收集、清除,和到數據庫的CRUD,也就不是難事了。
實戰篇:
按照這個方法,我在PDF.NET開發框架中實現了本文標題說的功能,最近還做了一個簡單的例子,大家可以去開源項目網站下載:
項目網址: http://pwmis.codeplex.com 到下載頁,選擇“ PDF.Net_V4.6 WinForm 數據表單實例 ”這個下載鏈接即可。
下面說說這個小程序的搭建過程,
1,新建項目
首先創建一個WinForm程序項目,引入下面幾個DLL類庫:
2,添加數據控件到工具箱
因為是WinForm項目,所以我們引用了PWMIS.Windows.dll, 它包含了我們需要的數據控件。
找到該文件,將它拖入我們的工具箱:
添加前,在工具箱中增加一個項:PDF.NET DataForm,然后在資源管理器中選擇Windows數據控件組件的文件,將它“拖放”到剛才建立的 PDF.NET DataForm下面
這是拖放后,添加PDF.NET Windows 數據控件成功后的工具箱樣子。
3,添加數據窗體
我們在主窗體上放置幾個按鈕和一個網格控件,以便增、刪、改、查詢數據:
然后我們再新建立一個窗體 Form2 ,在上面放置幾個我們需要的表單控件並設置好我們需要保存的表名稱和對應的字段名稱:
4,編寫代碼
4.1,基礎CRUD代碼
窗體建立好了,現在開始寫代碼,剛開始還沒有數據庫呢,這里我們是有Access數據庫文件,方便我們測試,在“創建數據庫”按鈕事件里面寫如下代碼:
private void btnCreateDB_Click(object sender, EventArgs e) { string dbpath = Application.StartupPath + "\\TEST.mdb"; if (!File.Exists(dbpath)) { //創建數據庫文件 PWMIS.AccessExtensions.AccessUility.CreateDataBase(dbpath); //創建表 Access access = new Access(); access.ConnectionString = "Provider=Microsoft.Jet.Oledb.4.0;Data Source=" + dbpath; PWMIS.AccessExtensions.AccessUility.CreateTable(access, new User()); //配置連接 PWMIS.AccessExtensions.AccessUility.ConfigConnectionSettings("AccessConn", dbpath); MessageBox.Show("創建數據成功!"); this.btnInsert.Enabled = true; this.btnUpdate.Enabled = true; this.btnDelete.Enabled = true; } else { MessageBox.Show("數據庫已經創建過了,如需重新創建,請先刪除數據庫文件。"); } }
注意,我們並沒有手工去創建數據表,而是利用事先定義好的PDF.NET實體類 User,在Access數據庫中自動創建了一個數據表的:
PWMIS.AccessExtensions.AccessUility.CreateTable(access, new User());
User實體類的定義很簡單,它內部指明了實體類將要映射到的表名和實體類屬性映射的字段名:

public class User:EntityBase { public User() { this.TableName = "會員用戶表"; this.IdentityName = "標識"; this.PrimaryKeys.Add("標識"); } protected override void SetFieldNames() { PropertyNames = new string[] {"標識","用戶名","用戶類型","注冊時間","消費金額" }; } public int UserID { get { return getProperty<int>("標識"); } set { setProperty("標識", value); } } public string UserName { get { return getProperty<string>("用戶名"); } set { setProperty("用戶名", value, 50); } } public int UserType { get { return getProperty<int>("用戶類型"); } set { setProperty("用戶類型", value); } } public DateTime RegisterDate { get { return getProperty<DateTime>("注冊時間"); } set { setProperty("注冊時間", value); } } public Single Expenditure { get { return getProperty<Single>("消費金額"); } set { setProperty("消費金額", value); } } }
實體類是事先手寫好的,表結構是后來程序運行時創建的,這也算是PDF.NET的CodeFirst 功能吧!
下面,寫主窗體的數據加載代碼:
List<User> list = OQL.From<User>().Select().END.ToList<User>(); this.dataGridView1.DataSource =list;
這里用上了PDF.NET框架的OQL擴展,一行代碼查詢數據,需要項目引用PWMIS.Core.Extensions.dll 以及
using PWMIS.Core.Extensions;
修改數據也是一行代碼:
User user = this.dataGridView1.CurrentRow.DataBoundItem as User; EntityQuery<User>.Instance.Update(user);
重頭戲在我們的Form2.cs 中,我們看看提交按鈕里面,是怎么收集、更新表單數據的:
private void btnSubmit_Click(object sender, EventArgs e) { //前面檢查數據的代碼略 var ibCommandList = MyWinForm.Instance.AutoUpdateIBFormData(this.Controls); }
就這一行代碼就足夠了,不需要使用任何實體類之類的,直接保存(Insert、Update)數據到數據庫,框架會自動判斷當前是新增還是修改,而根據就是看“主鍵數據控件”是否有值。
如果要清除表單數據,重新錄入數據也很簡單:
private void btnClear_Click(object sender, EventArgs e) { WinFormControlDataMap.ClearData(this.Controls); }
4.2,多窗體之間的數據同步
在我們這個小例子中,表單窗體(Form2)的數據變化后(新增、修改),可以立即反應到主窗體(Form1)上,而不用主窗體去重新加載數據,這里就必須用到數據綁定集合:
private BindingList<User> UserBindingList = new BindingList<User>(); //填充集合的代碼,就是將數據從數據庫查詢出來,然后放到該集合中,代碼略 this.dataGridView1.DataSource = UserBindingList;
光有BindingList<T> 集合還不夠,它的成員對象還必須實現“屬性更改通知”接口INotifyPropertyChanged,而PDF.NET的實體類正好實現了該接口:
public abstract class EntityBase : INotifyPropertyChanged, IEntity, ICloneable { //... 略 }
因此用PDF.NET的實體類來做WinForm、WPF、SL等窗體的數據Model是很合適的,適合在MVVM,MVP模式的項目中使用。
下面,使用框架提供的表單數據收集功能,就很容易的將數據收集到實體類,然后同步更新主窗體的列表數據了,也是一行代碼:
Form1 form1 = this.Owner as Form1; User user = form1.GetUserByID(int.Parse(dlbUID.Text)); //收集數據到實體類中 WinFormControlDataMap.CollectDataToEntityClass(user, this.Controls);
5,實例效果
最后,我們來看看這個功能的運行效果圖:
增加數據,在新窗體中錄入數據
單擊按鈕保存數據,主窗體列表中自動增加一行數據
新窗口先不關閉,修改下消費金額,確定,發現主窗口列表的數據被同步修改了。
整個過程沒有從數據庫去重新刷新數據到主窗口網格控件的,實現了多個窗體之見的數據同步。
-----------分界線------------------------