不使用反射,“一行程式碼”實現Web、WinForm窗體表單資料的填充、收集、清除,和到資料庫的CRUD
問題篇:
昨天在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類庫:
![](https://img.796t.com/res/2022/04-29/02/e4ed93a0271c222698685f106091d920.png)
2,新增資料控制元件到工具箱
因為是WinForm專案,所以我們引用了PWMIS.Windows.dll, 它包含了我們需要的資料控制元件。 找到該檔案,將它拖入我們的工具箱:
![](https://img.796t.com/res/2022/04-29/02/82ec9d9d3972432f71edf74348053398.png)
新增前,在工具箱中增加一個項:PDF.NET DataForm,然後在資源管理器中選擇Windows資料控制元件元件的檔案,將它“拖放”到剛才建立的 PDF.NET DataForm下面
![](https://img.796t.com/res/2022/04-29/02/39e670302207d59ddf92600ba9b06fe7.png)
這是拖放後,新增PDF.NET Windows 資料控制元件成功後的工具箱樣子。
3,新增資料窗體
我們在主窗體上放置幾個按鈕和一個網格控制元件,以便增、刪、改、查詢資料:
![](https://img.796t.com/res/2022/04-29/02/6d40891caffabef55ef61c99b951610f.png)
然後我們再新建立一個窗體 Form2 ,在上面放置幾個我們需要的表單控制元件並設定好我們需要儲存的表名稱和對應的欄位名稱:
![](https://img.796t.com/res/2022/04-29/02/ca79009f501f6f8c0b024f9cbf0778a8.png)
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,例項效果
最後,我們來看看這個功能的執行效果圖:
![](https://img.796t.com/res/2022/04-29/02/bee04a1d3905f74dfe6f8eaae03374c8.png)
增加資料,在新窗體中錄入資料
![](https://img.796t.com/res/2022/04-29/02/6155c657e8477fd8fcf34114d9c620b9.png)
單擊按鈕儲存資料,主窗體列表中自動增加一行資料
![](https://img.796t.com/res/2022/04-29/02/fce7c86ac774f26f988c55c2a31da8ab.png)
新視窗先不關閉,修改下消費金額,確定,發現主視窗列表的資料被同步修改了。
整個過程沒有從資料庫去重新重新整理資料到主視窗網格控制元件的,實現了多個窗體之見的資料同步。
-----------分界線------------------------