一句代码实现批量数据绑定[下篇]
《上篇》主要介绍如何通过DataBinder实现批量的数据绑定,以及如何解决常见的数据绑定问题,比如数据的格式化。接下来,我们主要来谈谈DataBinder的设计,看看它是如何做到将作为数据源实体的属性值绑定到界面对应的控件上的。此外,需要特别说明一点:《上篇》中提供了DataBinder最初版本的下载,但已经和本篇文章介绍的已经大不一样了。
最新版本的主要解决两个主要问题:通过Expression Tree的方式进行属性操作(属性赋值和取值),添加了“数据捕捉”(Data Capture)的功能,以实现将控件中的值赋给指定的实体。但是,这并不意味着这就是一个最终版本,这里面依然有一些问题,比如对空值的处理不不够全面,比如在进行数据绑定的时候,有的控件类型需要进行HTML Encoding,等等。[源代码从这里下载]
目录:
一、通过DataPropertyAttribute特性过滤实体的“数据属性”
二、Control/DataSource映射的表示:BindingMapping
三、如何建立Control/DataSource映射集合
四、通过映射集合实现数据绑定
五、通过映射集合实现数据捕捉
一、通过DataPropertyAttribute特性过滤实体的数据属性
DataBinder在进行数据绑定的时候,并没有对作为数据源的对象作任何限制,也就是说任何类型的对象均可作为数据绑定的数据源。控件(这里指TextBox、Label等这样绑定标量数值的控件)绑定值来源于数据源实体的某个属性。但是一个类型的属性可能有很多,我们需要某种筛选机制将我们需要的“数据属性”提取出来。这里我们是通过在属性上应用DataPropertyAttribute一个特性来实现的。
简单起见,我不曾为DataPropertyAttribute定义任何属性成员。DataPropertyAttribute中定义了一个静态的GetDataProperties方法,得到给定实体类型的所有数据属性的名称。但是为了避免频繁地对相同实体类型进行反射,该方法对得到的属性名称数组进行了缓存。
[AttributeUsage( AttributeTargets.Property, AllowMultiple = false,Inherited = true)]
public class DataPropertyAttribute: Attribute
{
private static Dictionary<Type, string[]> dataProperties = new Dictionary<Type, string[]>();
public static string[] GetDataProperties(Type entityType)
{
Guard.ArgumentNotNullOrEmpty(entityType, "entityType");
if (dataProperties.ContainsKey(entityType))
{
return dataProperties[entityType];
}
lock (typeof(DataPropertyAttribute))
{
if (dataProperties.ContainsKey(entityType))
{
return dataProperties[entityType];
}
var properties = (from property in entityType.GetProperties()
where property.GetCustomAttributes(typeof(DataPropertyAttribute), true).Any()
select property.Name).ToArray();
dataProperties[entityType] = properties;
return properties;
}
}
}
二、Control/DataSource映射的表示:BindingMapping
不论是数据绑定(实体=〉控件),还是数据捕捉(控件=〉实体)的实现都建立在两种之间存在着某种约定的映射之上,这个映射是整个DataBinder的核心所在。在这里,我定义了如下一个BindingMapping类型表示这个映射关系。
public class BindingMapping: ICloneable
{
public Type DataSourceType { get; private set; }
public Control Control { get; set; }
public string ControlValueProperty { get; set; }
public string DataSourceProperty { get; set; }
public bool AutomaticBind { get; set; }
public bool AutomaticUpdate { get; set; }
public string FormatString { get; set; }
public Type ControlValuePropertyType
{
get { return PropertyAccessor.GetPropertyType(this.Control.GetType(), this.ControlValueProperty); }
}
public Type DataSourcePropertyType
{
get { return PropertyAccessor.GetPropertyType(this.DataSourceType, this.DataSourceProperty); }
}
public BindingMapping(Type dataSourceType, Control control, string controlValueProperty, string dataSourceProperty)
{
//...
this.DataSourceType = dataSourceType;
this.Control = control;
this.ControlValueProperty = controlValueProperty;
this.DataSourceProperty = dataSourceProperty;
this.AutomaticBind = true;
this.AutomaticUpdate = true;
}
object ICloneable.Clone()
{
return this.Clone();
}
public BindingMapping Clone()
{
var bindingMapping = new BindingMapping(this.DataSourceType, this.Control, this.ControlValueProperty, this.DataSourceProperty);
bindingMapping.AutomaticBind = this.AutomaticBind;
bindingMapping.AutomaticUpdate = this.AutomaticBind;
return bindingMapping;
}
}
这里我主要介绍一下各个属性的含义:
- DataSourceType:作为数据源实体的类型;
- Control:需要绑定的控件;
- ControlValueProperty:数据需要绑定到控件属性的名称,比如TextBox是Text属性,而RadioButtonList则是SelectedValue属性;
- DataSourceProperty:实体类型中的数据属性名称
- AutomaticBind:是否需要进行自动绑定,通过它阻止不必要的自动数据绑定行为。默认值为True,如果改成False,基于该条映射的绑定将被忽略;
- AutomaticUpdate:是否需要进行自动更新到数据实体中,通过它阻止不必要的自动数据捕捉行为。默认值为True,如果改成False,基于该条映射的数据捕捉定将被忽略;
- FormatString:格式化字符串;
- ControlValuePropertyType:控件绑定属性的类型,比如TextBox的绑定属性为Text,那么ControlValuePropertyType为System.String;
- DataSourcePropertyType:实体属性类型。
需要补充一点的是:ControlValuePropertyType和DataSourcePropertyType使用到了之前定义的用于操作操作属性的组件ProcessAccessor。BindingMapping采用了克隆模式。
三、如何建立Control/DataSource映射集合
BindingMapping表示的一个实体类型的数据属性和具体控件之间的映射关系,而这种关系在使用过程中是以批量的方式进行创建的。具体来说,我们通过指定实体类型和一个作为容器的空间,如果容器中的存在满足映射规则的子控件,相应的映射会被创建。映射的批量创建是通过DataBinder的静态方法BuildBindingMappings来实现的。
在具体介绍BuildBindingMappings方法之前,我们需要先来讨论一个相关的话题:在进行数据绑定的时候,如何决定数据应该赋值给控件的那个属性。我们知道,不同的控件类型拥有不同的数据绑定属性,比如TextBox自然是Text属性,CheckBox则是Checked属性。ASP.NET在定义控件类型的时候,采用了一个特殊性的特性ControlValuePropertyAttribute来表示那个属性表示的是控件的“值”。比如TextBox和CheckBox分别是这样定义的。
[ControlValueProperty("Text")]
public class TextBox : WebControl, IPostBackDataHandler, IEditableTextControl, ITextControl
{
//...
}
ControlValueProperty("Checked")]
public class CheckBox : WebControl, IPostBackDataHandler, ICheckBoxControl
{
//...
}
在这里我们直接将ControlValuePropertyAttribute中指定的名称作为控件绑定的属性名,即BindingMapping的ControlValueProperty属性。该值得获取通过如下一个GetControlValuePropertyName私有方法完成。为了避免重复反射操作,这里采用了全局缓存。
private static string GetControlValuePropertyName(Control control)
{
if (null == control)
{
return null;
}
Type entityType = control.GetType();
if (controlValueProperties.ContainsKey(entityType))
{
return controlValueProperties[entityType];
}
lock (typeof(DataBinder))
{
if (controlValueProperties.ContainsKey(entityType))
{
return controlValueProperties[entityType];
}
ControlValuePropertyAttribute controlValuePropertyAttribute = (ControlValuePropertyAttribute)entityType.GetCustomAttributes(typeof(ControlValuePropertyAttribute), true)[0];
controlValueProperties[entityType] = controlValuePropertyAttribute.Name;
return controlValuePropertyAttribute.Name;
}
}
最终的映射通过如下定义的BuildBindingMappings方法来建立,缺省参数suffix代表的是控件的后缀,其中已经在《上篇》介绍过了。
public static IEnumerable<BindingMapping> BuildBindingMappings(Type entityType, Control container, string suffix = "")
{
//...
suffix = suffix??string.Empty;
return (from property in DataPropertyAttribute.GetDataProperties(entityType)
let control = container.FindControl(string.Format("{1}{0}", suffix, property))
let controlValueProperty = GetControlValuePropertyName(control)
where null != control
select new BindingMapping(entityType, control, controlValueProperty, property)).ToArray();
}
四、通过映射集合实现数据绑定
通过《上篇》我们知道,DataBinder提供两种数据绑定方式:一种是直接通过传入数据实体对象和容器控件对具有匹配关系的所有子控件进行绑定;另外一种则是通过调用上面BuildBindingMappings静态方法建立的BindingMapping集合,然后再借助于这个集合进行数据绑定。这两种方式的数据绑定对应于如下两个重载的BindData方法:
public class DataBinder
{
//...
public void BindData(object entity, Control container, string suffix = "");
public void BindData(object entity,IEnumerable<BindingMapping> bindingMappings);
}
已经上在内部,上面一个方法也是需要通过调用BuildBindingMappings来建立映射。数据绑定始终是根据BindingMapping集合进行的。由于在BindingMapping中已经定义了完成数据绑定所需的必要信息,数据绑定的逻辑变得很简单。具体来说,数据绑定的逻辑是这样的:遍历所有的集合中每个BindingMapping,根据DataSourceProperty得到属性名称,然后进一步从数据源实体中得到具体的值。
根据ControlValuePropertyType得到目标控件绑定属性的类型,然后将之前得到的值转换成该类型。最后,通过ControlValueProperty得到控件的绑定属性,将之前经过转换的值给控件的这个属性就可以了。整个数据绑定实现在如下一个OnBindData方法中。关于属性操作则借助于PropertyAccessor这个组件。
protected virtual void OnBindData(IEnumerable<BindingMapping> bindingMappings, object entity)
{
foreach (var mapping in bindingMappings)
{
var bindingMapping = mapping.Clone();
object value = PropertyAccessor.Get(entity, bindingMapping.DataSourceProperty);
if (null != this.DataItemBinding)
{
var args = new DataBindingEventArgs(bindingMapping, value);
this.DataItemBinding(this, args);
value = args.DataValue;
}
if (!bindingMapping.AutomaticBind)
{
continue;
}
if (!string.IsNullOrEmpty(bindingMapping.FormatString))
{
value = Format(value, bindingMapping.FormatString);
}
Type controlValuePropertyType = PropertyAccessor.GetPropertyType(bindingMapping.Control.GetType(), bindingMapping.ControlValueProperty);
value = ChangeType(value, controlValuePropertyType);
if (null == value && typeof(ValueType).IsAssignableFrom(controlValuePropertyType))
{
value = Activator.CreateInstance(controlValuePropertyType);
}
PropertyAccessor.Set(bindingMapping.Control, bindingMapping.ControlValueProperty, value);
if (null != this.DataItemBound)
{
this.DataItemBound(this, new DataBindingEventArgs(bindingMapping, value));
}
}
}
DataBinder设计的目标是让默认的绑定行为解决80%的问题,并且提供给相应的方式去解决余下的问题。为了让开发者能够有效解决余下的这20%的绑定问题,我们定义两个事件:DataItemBinding和DataBound,它们分别在进行绑定之前和之后被触发。关于事件的触发,已经体现在OnBindData方法的定义中了。
五、通过映射集合实现数据捕捉
数据绑定使用到的实际上是Entity-〉Control映射,如果我们借助控件到Control-〉Entity,就能实现自动捕获控件的值然后将其保存到给定的实体对象上。我为此在DataBinder上定义了两个重载的UpdateData方法。
public class DataBinder
{
//...
public void BindData( object entity,IEnumerable<BindingMapping> bindingMappings);
public void UpdateData( object entity, Control container, string suffix = "");
}
UpdateData方法的实现和BindData方法的逻辑基本一致,将Control和Entity呼唤一下而已,所以在这里我就不再赘言叙述了。