昨天在CSDN看到这样一个帖子:“苦逼的三层代码”:
采用传统的三层架构写代码,每个数据表都要定义一个实体对象,编写后台的时候, Web层需要针对页面的用户输入逐个手动编写赋值到实体对象的各个属性,然后DAL层还要用SqlHelper 进行各个存储过程对应参数的实体赋值, 我的天呀,写几个表还好,多个表呢, 写的后台都没力气, 典型的苦逼代码工没营养,各位有啥好的处理方法或开发方式。。
看到跟帖,大部分都说使用ORM解决这个问题,但我觉得ORM还是没有解决贴主的几个问题:
另外跟帖中也有不少上用动软的三层代码生成器,这个方法看似能够解决一部分问题,但必须使用代码生成器规定的那种三层结构,不利于灵活扩展,而且遇到业务稍复杂的情况,也不是代码生成器能够解决的问题。
实际上,对于问题1,问题2,我们按照一定规则,使用反射是可以解决对象属性手工逐个赋值、取值的过程的,需要我们自己好好制定这个规则。这里我采用另外一种方案,不使用反射,“一行代码”实现Web、WinForm窗体表单数据的填充、收集、清除,和到数据库的CRUD,而秘诀就是对表单控件进行扩展。
我们常用的表单控件主要有以下几个:
我们对这些控件进行扩展,让它统一继承一个数据接口 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 数据表单实例 ”这个下载链接即可。
下面说说这个小程序的搭建过程,
首先创建一个WinForm程序项目,引入下面几个DLL类库:
因为是WinForm项目,所以我们引用了PWMIS.Windows.dll, 它包含了我们需要的数据控件。 找到该文件,将它拖入我们的工具箱:
添加前,在工具箱中增加一个项:PDF.NET DataForm,然后在资源管理器中选择Windows数据控件组件的文件,将它“拖放”到刚才建立的 PDF.NET DataForm下面
这是拖放后,添加PDF.NET Windows 数据控件成功后的工具箱样子。
我们在主窗体上放置几个按钮和一个网格控件,以便增、删、改、查询数据:
然后我们再新建立一个窗体 Form2 ,在上面放置几个我们需要的表单控件并设置好我们需要保存的表名称和对应的字段名称:
窗体建立好了,现在开始写代码,刚开始还没有数据库呢,这里我们是有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);
}
在我们这个小例子中,表单窗体(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);
最后,我们来看看这个功能的运行效果图:
增加数据,在新窗体中录入数据
单击按钮保存数据,主窗体列表中自动增加一行数据
新窗口先不关闭,修改下消费金额,确定,发现主窗口列表的数据被同步修改了。
整个过程没有从数据库去重新刷新数据到主窗口网格控件的,实现了多个窗体之见的数据同步。
-----------分界线------------------------