深入ASP.NET数据绑定
在ASP.NET我们在使用Repeater,DetailsView,FormView,GridView等数据绑定模板时,都会使用<%# Eval("字段名") %>或<%# Bind("字段名") %>这样的语法来单向或双向绑定数据。但是我们却很少去了解,在这些语法的背后,ASP.NET究竟都做了哪些事情来方便我们使用这样的语法来绑定数据。究竟解析这样的语法是在编译时,还是运行时?如果没有深入去了解,我们肯定不得而知。这个简短的系列文章就是带我们大家一起去深入探究一下ASP.NET绑定语法的内部机理,以让我们更加全面的认识和运用它。
事件的起因是,我希望动态的为Repeater控件添加行项模板,我可以通过实现ITempate接口的方式来动态添加行模板。并希望它通过普通的页面绑定语法来完成数据字段的绑定功能,如下就是一个简单的例子:
/// <summary>
/// Summary description for DynamicTemplate
/// </summary>
public class DynamicTemplate : ITemplate
{
public DynamicTemplate()
{
//
// TODO: Add constructor logic here
//
}
#region ITemplate Members
public void InstantiateIn(Control container)
{
TextBox textBox = new TextBox();
textBox.Text = @"<%# Eval(""ID"") %>";
container.Controls.Add(textBox);
}
#endregion
}
在这个例子中,我在模板中添加了一个TextBox控件,并指定它的绑定字段是“ID”。但是这做法,能否实现我们实现我们需要的功能呢?答案是否定,每一行的TextBox的值都是"<%# Eval(""ID"") %>",而不会像我们希望的那样去绑定ID字段。从结果来分析原因,我们可以非常容易得出,这段绑定语法并没有得到ASP.NET运行时的承认,那么页面中使用相同的语法为什么可以呢?故事就是从这里开始的。
我们首先要去了解下,在页面中使用这样的语法ASP.NET都为我们做了哪些事情呢?要了解这个,我们要找到.aspx文件在首次运行时动态编译的程序集。
我们都知道,在ASP.NET运行时,也会把.aspx文件编译成一个动态类,这个类是继承于.aspx的Page指令中Inherits属性指定的类并且同时也直接实现了IHttpHandler接口。这个动态类会负责创建页面中使用的各种服务器端控件的实例,并且ASP.NET运行时会负责解析的编译.aspx中存在的服务器端代码(包括绑定语法)并将这些代码编译到这个页面类。WebSite工程和Web Application在页面文件上有些不同,WebSite工程的每个页面最多可以有两个文件:.aspx和.aspx.cs文件;而在Web Application还可以包括.aspx.designer.cs文件,这个文件所起的作用也非常有限,也就是为了能在页面代码中使用服务器端、控件实例而定义的一个实例变量,仅此而已。所以在设计时WebSite具备更多的动态行为,而在运行时WebSite工程和Web Application并没有太大区别。
如何得到页面的动态类呢?要首先得到这个页所在的动态程序集,在Vista以前的操作系统上,一般是在:%SystemRoot%\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files 文件夹下,而在Vista中,而会在:%USERPROFILE%\AppData\Local\Temp\Temporary ASP.NET Files下。那么如何快速得到程序集的路径和名称?你可以让你的Web工程动态编译出错(比如重复的类名),就可以快速定位到当前动态程序集的目录了。
动态类中会有很多的内容,我们不作更多的分析,我们把目光集中绑定代码上。假设现在页面上有这么一段Repeater绑定代码:
<asp:Repeater runat="server" ID="repeater">
<HeaderTemplate>
<table>
<tr>
<td>
ID
</td>
<td>
电流{a}
</td>
<td>电压(V)</td>
<td>
备注'
</td>
<td>
名称]
</td>
</tr>
</HeaderTemplate>
<ItemTemplate>
<tr>
<td>
<%# Eval("ID")%>
</td>
<td>
<%# Eval("电流{a}")%>
</td>
<td><%# Eval("电压(V)")%></td>
<td>
<%# Eval("备注'")%>
</td>
<td>
<%# Eval("名称]")%>
</td>
</tr>
</ItemTemplate>
<FooterTemplate>
</table>
</FooterTemplate>
</asp:Repeater>
那么在动态类中,相应的会有这样的一段函数,是用来创建ID为repeater的控件实例:
[DebuggerNonUserCode]
private Repeater __BuildControlrepeater()
{
Repeater repeater = new Repeater();
base.repeater = repeater;
repeater.HeaderTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control4));
repeater.ItemTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control5));
repeater.FooterTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control7));
repeater.ID = "repeater";
return repeater;
}
CompiledTempateBuilder和BuildTemplateMethod只是模板实例化的一个中介,真正用于添加模板内容的是后面的那些私有函数,如ItemTempate的模板内容实例的创建就在__BuildControl__control5函数中,这个函数原型定义是:
[DebuggerNonUserCode]
private void __BuildControl__control5(Control __ctrl)
{
DataBoundLiteralControl control = this.__BuildControl__control6();
IParserAccessor accessor = __ctrl;
accessor.AddParsedSubObject(control);
}
在这个函数里,调用了另一个私有函数this.__BuildControl__control6,这个函数返回的一个DataBoundLiteralControl对象,并将对象输出添加到__ctrl参数。事实上,只要我们去阅读CompiledTempateBuilder就发现在,这里的__ctrol对象就是我们在实例化模板时传入的对象,也就是ITemplate中的InstantiateIn方法的那个container参数对象。
为什么使用的是AddParsedSubObject方法,使用这个方法添加子控件相当于告诉父控件,这是一个已经解析好的子控件对象,不需再去将控件解析成HTML代码,而在输出时直接输出Text属性的值即可。从这里我们还可以得知DataBoundLiteralControl的对象,事实上就是承担了字符串拼接的职责,这一点我们可以在后面的分析中得以验证。
__BuildControl__control6私有函数的定义如下:
[DebuggerNonUserCode]
private DataBoundLiteralControl __BuildControl__control6()
{
DataBoundLiteralControl control = new DataBoundLiteralControl(5, 4);
control.TemplateControl = this;
control.SetStaticString(0, "\r\n <tr>\r\n <td>\r\n ");
control.SetStaticString(1, "\r\n </td>\r\n <td>\r\n ");
control.SetStaticString(2, "\r\n </td>\r\n \r\n <td>\r\n ");
control.SetStaticString(3, "\r\n </td>\r\n <td>\r\n ");
control.SetStaticString(4, "\r\n </td>\r\n </tr>\r\n ");
control.DataBinding += new EventHandler(this.__DataBind__control6);
return control;
}
在这个函数里面,创建了一个DataBoundLiteralControl对象,并将页面上定义的模板的静态HTML代码添加到该的静态字符串数组里,并且设置了它的绑定事件代理函数__DataBind__control6,该函数的定义:
public void __DataBind__control6(object sender, EventArgs e)
{
DataBoundLiteralControl control = (DataBoundLiteralControl) sender;
RepeaterItem bindingContainer = (RepeaterItem) control.BindingContainer;
control.SetDataBoundString(0, Convert.ToString(base.Eval("ID"), CultureInfo.CurrentCulture));
control.SetDataBoundString(1, Convert.ToString(base.Eval("电流{a}"), CultureInfo.CurrentCulture));
control.SetDataBoundString(2, Convert.ToString(base.Eval("备注'"), CultureInfo.CurrentCulture));
control.SetDataBoundString(3, Convert.ToString(base.Eval("名称]"), CultureInfo.CurrentCulture));
}
在这个函数中,我们看到了真正的数据绑定代码了,它调用了TemplateControl的Eval方法来将当前数据项的相应字段的值取出,并按一定的格式转化后添加到DataBoundLitreralControl对象中,并在DataBoundLiteralControl将StaticString和DataBoundString字符串数组按一定的顺序拼接起来,作为Text属性的输出值。而容器控件则直接向客户端输这段HTML。
下面,我们还有必要来分析下TemplateControl中的Eval方法,这个方法有两种重载,简单起见,我们来分析较为简单的重载:
protected internal object Eval(string expression)
{
this.CheckPageExists();
return DataBinder.Eval(this.Page.GetDataItem(), expression);
}
这个方法,使用了DataBinder.Eval静态方法来得到绑定表达式(字段名)的值,它的数据是通过this.Page.GetDataItem()这样的一个方法得到的。那么为什么this.Page.GetDataItem()就可以得到当前正在被绑定的数据项呢?原来,在页面绑定数据时,它会有一个堆栈来保存它所有的绑定控件绑定时用到的数据项,我们只需要取得堆栈顶部的那个元素,就可以在页面的作用域内的任何一个位置得到当前正在被绑定的数据项。如上的例子,我们就可以取得当前绑定的RepeaterItem的DataItem的数据项,因此我们不需要与RepeaterItem有任何的联系。
如果硬要用上面的代码来描述数据绑定的全过程,跨度过大。但是有了以上的分析,我们再用文字的形式再来总结下,应该就会一个比较完整的印象了:在ASP.NET的数据模板控件中,可以使用<%# %>这样的语法来将字段值作为一个占位符,用在HTML代码中,可以方便我们设计和生成最终的HTML代码,不需要很多的字符拼接工作。而ASP.NET运行时在首次执行页面时,会为页面编译一个动态类,在这个动态类中会实例化所有的服务器端控件,编译和解析绑据模板控件的绑定语法,并用一些对象和操作来完成数据绑定的字符串接拼接行为。因此绑定语法的解析事实上是编译时的行为,只不过这个编译时是延迟到页面的首次执行时。这就可以解释为什么在我们想在动态添加模板中使用<%# %>这样的绑定语法时,无法解析的原因。
而对于DataBinder.Eval方法,这是ASP.NET提供的一个数据绑定辅助方法。通过这个方法,我们可以方便的从种不同的数据项,如自定义对象或DataRow取出对象的字段(属性值)。从而为我们屏蔽很多不必要的数据来源类型的判断。同时DataBinder这个类还提供了其它的绑定辅助方法,大家可以从MSDN查看更多有用的帮助。
以上我们主要讨论了Eval的单向数据绑定。
在.NET 2.0中新增了双向的数据绑定方式,主要用在GridView,DetailsView,FormView等数据容器控件中,结合DataSourceControl就可以非常轻松的完成数据的更新和提交工作,而不需要我们手工去遍历输入控件的值。那在这样的双向数据绑定中,ASP.NET又是做了哪些工作,来为我们透明输入控件与字段的取值与对应关系,让我们可以在DataSouceControl中方便得到数据项修改前的值和修改后的值?下面就让我们一起来从一段页面代码开始吧:
<asp:DetailsDataSouce ID="DetailsDataSouce1" runat="server">
</asp:DetailsDataSouce>
<asp:DetailsView ID="detailsView" runat="server" DefaultMode="Edit" DataSourceID="DetailsDataSouce1">
<Fields>
<asp:TemplateField>
<HeaderTemplate>
电流:</HeaderTemplate>
<EditItemTemplate>
<asp:TextBox ID="textBox1" runat="server" Text='<%# Bind("[电流{a}]")%>'></asp:TextBox>
</EditItemTemplate>
</asp:TemplateField>
</Fields>
</asp:DetailsView>
在一个页面中,定义了如上的一个DetailsView控件,为这个控件指定了ID为DetailsDataSource1的DataSouceControl控件,这个控件是我们自己定义的一个DataSourceControl,它返回的数据字段包括:"ID","电流{a}","电压(v)","备注'","名称]"。我并没有设置DetailsView的AutoGenerateRows属性的值,默认情况下,它是为我们自动的生成这些字段的对应的数据显示和输入控件。除此之外,我们还另外添加了一个数据模板字段,在这个模板中指定了编辑模板。在编辑模板中我使用了<%# Bind("")%>这样的语法,将textBox1与"[电流{a}]"字段双向绑定起来。
为什么这里的字段都有一些特殊呢?因为我原先的意图是除了分析绑定语法以外,还要测试哪些特殊字符无法使用数据绑定语法来绑定数据的。这个在下篇文章中会具体介绍。
Bind与Eval不一样,这样的Bind并不Page或TemplateControl的一个方法,事实上我们应该把它当成一个关键字来看待,因为在ASP.NET的双向数据绑定当中,并没有这样的一个函数存在,它的存在是只是告诉ASP.NET动态编译页面类时,将这个语法编译成一定的代码格式,并生成一些函数代理来达到双向数据交流的目的。
那么这一段代码,动态编译生成的服务器代码又是如何的呢?让我们反编译动态程序集,里面会找到用于创建DetailsView的__BuildControldetailsView的私有方法,在这里会调用到一些其它内部方法,我们不要让这些方法来干扰我们的视线,直接找到创建如上模板字段的方法:
[DebuggerNonUserCode]
private TemplateField __BuildControl__control5()
{
TemplateField field = new TemplateField();
field.HeaderTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control6));
field.EditItemTemplate = new CompiledBindableTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control7), new ExtractTemplateValuesMethod(this.__ExtractValues__control7));
return field;
}
这里首先把this.__BuildControl__control6作为一个代理函数,用于创建头部模板的内容,也就是如上的“电流:”字段标题。然后才是创建EditItemTemplate,这个模板又被一些的中介模板所代替,我们只需要来关心this.__BuildControl__control7和__ExtractValues__control7即可。__BuildControl__control7是为了编辑数据字段时,将数据字段的值显示在输入控件中(输入控件的初始化,即字段值绑定到输入控件中);而__ExtractValues__control7则是在提交数据时,要找出这个模板内所有的双向绑定字段,将这些字段的值以绑定字段名为Key,以输入控件的值为Value添加了IOrderedDictionary字典中。DetailsView等数据绑定控件调用这些委托代理来收集所有的被双向绑定的字段的最新的值。下面分别是两段函数的代码片段:
[DebuggerNonUserCode]
private TextBox __BuildControl__control8()
{
TextBox box = new TextBox();
box.TemplateControl = this;
box.ApplyStyleSheetSkin(this);
box.ID = "textBox1";
box.DataBinding += new EventHandler(this.__DataBinding__control8);
return box;
}
public void __DataBinding__control8(object sender, EventArgs e)
{
TextBox box = (TextBox) sender;
IDataItemContainer bindingContainer = (IDataItemContainer) box.BindingContainer;
if (this.Page.GetDataItem()!= null)
{
box.Text = Convert.ToString(base.Eval("[电流{a}]"), CultureInfo.CurrentCulture);
}
}
[DebuggerNonUserCode]
public IOrderedDictionary __ExtractValues__control7(Control __container)
{
TextBox box = (TextBox) __container.FindControl("textBox1");
OrderedDictionary dictionary = new OrderedDictionary();
if (box!= null)
{
dictionary["[电流{a}]"] = box.Text;
}
return dictionary;
}
由上面的代码片段可以了解到,ASP.NET动态编译器是将Bind语法拆分为两部分:绑定输出和读取输入控件值。绑定输出部分与前篇介绍的机制是完全一样的,并且也是调用DataBinder.Eval方法来绑定数据;而读取输入控件值则是会根据页面上控件的类型,以及绑定的控件属性名称,生成一段强类型的控件属性读取代码,并将控件的值保存到dictionay中返回出去。而它全然不知,容器控件是如何将这些值合并起来传给对应的DataSouceControl控件的。
关于数据容器控件而何与DataSouceControl协同工作,并不是我们这里要分析的重点。但是我们可以简单的描述一下工作流程,以DetailsView的数据更新为例:大家通过反编译DetailsView的源码,会找到名称为HandleUpdate的私有方法,在这个方法里面会去处理数据项更新前的值(至于在Web环境中如何保存更新前的值,就需要靠ViewState的强大功能了),和更新后的值(通过ExtractRowValues函数调用类似上面生成的__ExtractValues__control7代理函数来收集所有双向绑定字段的值存到NewValues里面),并将他们分别保存在两个不同的IOrderedDictionary对象(OldValues,NewValues)中。然后将调用对应的DataSouceView的Update方法,传入原字段值和新字段值和一些必须的参数,即可由我们通过重写DataSourceView的方法来得到所有需要更新字段的原始值和新值,并可以对比比较哪些字段值是否发生了变化。NBearDataSource控件就是利用了这样的机制来直接重DataSourceControl和DataSourceView来达到数据的全自动修改和添加方案的。
这里还有一点不得不说,在GridView,DetailsView,并不一定需要使用<%# Bind("")%>语法来实现数据的双向绑定,他们的字段双向绑定可以通过BoundField及它的子控件代替模板控件的绑定语法,一样可以达到双向绑定的目的,简单但没有模板来得灵活。而在存取不同版本的字段值也是类似的机制。
由于这部分涉及到的都是动态和内部代码,如果没有亲自去阅读这些代码,估计还是很难理解。最后我们再来简单总结一下:ASP.NET在模板中双向绑定字段,是通过<%# Bind()%>这样的语法,但是Bind我们更应该把它理解为是一个关键字,而不是一个函数。因为在ASP.NET的控件中,并没有存在这个函数。ASP.NET运行时在编译页面代码时,会把Bind关键字的代码当成两部分来编译:一部分是单向绑定代码;另一部分而是读取对应输入控件的绑定属性,以绑定字段名为Key,添加到IOrderedDictionary中收集返回给数据容器控件(GridView,DetailsView,FormView)等,让它们处理。
总体来说,ASP.NET 2.0的双向绑定机制给我们在提交数据时带来了极大的方便,尽管有些人很排斥DataSourceControl的模式,但是我们不可否认合理应用会大大提高我们的开发效率。附上示例工程,本文分析面页是Default3.aspx和App_Web_ryn6wtvv.dll程序集。
在了解了数据绑定语法的原理后,我还想来谈谈我中实践过程中遇到的一些问题以及其它实用的绑定技巧。首先我们就来说说,特殊字段名的问题。我们知道在数据库当中,如果表名或字段名中包含有一些特殊的不能是合法的字符时,都会使用[]将它们引起来,以便他们能够正常使用。但是在<%# Eval("")%>的绑定语句当中,同时可以使用[],但是对于字段名中包含"(",")","[","]"这4个字符却始终运行出错。假设像我下面这样来绑定"电压(V)":<%# Eval("[电压(V)]")%>
那么就会得到一个运行时错误:
DataBinding:“System.Data.DataRowView”不包含名为“电压”的属性。
表明括号是被认为是一个特殊字符,那我们如果给字段名加上[],如下:<%# Eval("[电压(V)]")%>
此时,我们会得到另一个运行时错误:
电压(V 既不是表 DataTable1 的 DataColumn 也不是 DataRelation。
表明,即使加上[]也无法解决这个特殊字段名的问题。同时字段名中如果也存在中括号,也是会出现这样的问题的。但是这样的字段名却在GridView的自动生成列中能被正常绑定呢?问题会出现在哪里呢?分析和对比GridView的自动生成列与Eval这样的绑定语法在最终执行绑定代码上的不同,我们可以发现,GridView的自动生成列取值并不是使用DataBinder.Eval这个方法,它内部有自己的取值方式,但是在实现上却是大同小异的。那究竟是在哪里出现了问题呢?我们找出DataBinder类的定义:
[AspNetHostingPermission(SecurityAction.LinkDemand, Level=200)]
public sealed class DataBinder
{
// Fields
private static readonly char[] expressionPartSeparator = new char[] { '.' };
private static readonly char[] indexExprEndChars = new char[] { ']', ')' };
private static readonly char[] indexExprStartChars = new char[] { '[', '(' };
// Methods
public static object Eval(object container, string expression)
{
if (expression == null)
{
throw new ArgumentNullException("expression");
}
expression = expression.Trim();
if (expression.Length == 0)
{
throw new ArgumentNullException("expression");
}
if (container == null)
{
return null;
}
string[] expressionParts = expression.Split(expressionPartSeparator);
return Eval(container, expressionParts);
}
private static object Eval(object container, string[] expressionParts)
{
object propertyValue = container;
for (int i = 0; (i < expressionParts.Length)&& (propertyValue != null); i++)
{
string propName = expressionParts[i];
if (propName.IndexOfAny(indexExprStartChars)< 0)
{
propertyValue = GetPropertyValue(propertyValue, propName);
}
else
{
propertyValue = GetIndexedPropertyValue(propertyValue, propName);
}
}
return propertyValue;
}
public static string Eval(object container, string expression, string format)
{
object obj2 = Eval(container, expression);
if ((obj2 == null)||(obj2 == DBNull.Value))
{
return string.Empty;
}
if (string.IsNullOrEmpty(format))
{
return obj2.ToString();
}
return string.Format(format, obj2);
}
public static object GetDataItem(object container)
{
bool flag;
return GetDataItem(container, out flag);
}
public static object GetDataItem(object container, out bool foundDataItem)
{
if (container == null)
{
foundDataItem = false;
return null;
}
IDataItemContainer container2 = container as IDataItemContainer;
if (container2 != null)
{
foundDataItem = true;
return container2.DataItem;
}
string name = "DataItem";
PropertyInfo property = container.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
if (property == null)
{
foundDataItem = false;
return null;
}
foundDataItem = true;
return property.GetValue(container, null);
}
public static object GetIndexedPropertyValue(object container, string expr)
{
if (container == null)
{
throw new ArgumentNullException("container");
}
if (string.IsNullOrEmpty(expr))
{
throw new ArgumentNullException("expr");
}
object obj2 = null;
bool flag = false;
int length = expr.IndexOfAny(indexExprStartChars);
int num2 = expr.IndexOfAny(indexExprEndChars, length + 1);
if (((length < 0)||(num2 < 0))||(num2 == (length + 1)))
{
throw new ArgumentException(SR.GetString("DataBinder_Invalid_Indexed_Expr", new object[] { expr }));
}
string propName = null;
object obj3 = null;
111: string s = expr.Substring(length + 1,(num2 - length)- 1).Trim();
if (length != 0)
{
propName = expr.Substring(0, length);
}
if (s.Length != 0)
{
if (((s[0] == '"')&& (s[s.Length - 1] == '"'))||((s[0] == '\'')&& (s[s.Length - 1] == '\'')))
{
obj3 = s.Substring(1, s.Length - 2);
}
else if (char.IsDigit(s[0]))
{
int num3;
flag = int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out num3);
if (flag)
{
obj3 = num3;
}
else
{
obj3 = s;
}
}
else
{
obj3 = s;
}
}
if (obj3 == null)
{
throw new ArgumentException(SR.GetString("DataBinder_Invalid_Indexed_Expr", new object[] { expr }));
}
object propertyValue = null;
if ((propName != null)&& (propName.Length != 0))
{
propertyValue = GetPropertyValue(container, propName);
}
else
{
propertyValue = container;
}
if (propertyValue == null)
{
return obj2;
}
Array array = propertyValue as Array;
if ((array!= null)&& flag)
{
return array.GetValue((int) obj3);
}
if ((propertyValue is IList)&& flag)
{
return ((IList) propertyValue)[(int) obj3];
}
PropertyInfo info = propertyValue.GetType().GetProperty("Item", BindingFlags.Public | BindingFlags.Instance, null, null, new Type[] { obj3.GetType()}, null);
if (info == null)
{
throw new ArgumentException(SR.GetString("DataBinder_No_Indexed_Accessor", new object[] { propertyValue.GetType().FullName }));
}
return info.GetValue(propertyValue, new object[] { obj3 });
}
public static string GetIndexedPropertyValue(object container, string propName, string format)
{
object indexedPropertyValue = GetIndexedPropertyValue(container, propName);
if ((indexedPropertyValue == null)||(indexedPropertyValue == DBNull.Value))
{
return string.Empty;
}
if (string.IsNullOrEmpty(format))
{
return indexedPropertyValue.ToString();
}
return string.Format(format, indexedPropertyValue);
}
public static object GetPropertyValue(object container, string propName)
{
if (container == null)
{
throw new ArgumentNullException("container");
}
if (string.IsNullOrEmpty(propName))
{
throw new ArgumentNullException("propName");
}
PropertyDescriptor descriptor = TypeDescriptor.GetProperties(container).Find(propName, true);
if (descriptor == null)
{
throw new HttpException(SR.GetString("DataBinder_Prop_Not_Found", new object[] { container.GetType().FullName, propName }));
}
return descriptor.GetValue(container);
}
public static string GetPropertyValue(object container, string propName, string format)
{
object propertyValue = GetPropertyValue(container, propName);
if ((propertyValue == null)||(propertyValue == DBNull.Value))
{
return string.Empty;
}
if (string.IsNullOrEmpty(format))
{
return propertyValue.ToString();
}
return string.Format(format, propertyValue);
}
internal static bool IsNull(object value)
{
if ((value != null)&& !Convert.IsDBNull(value))
{
return false;
}
return true;
}
}
其中我们可以发现有三个静态只读变量:
private static readonly char[] expressionPartSeparator = new char[] { '.' };
private static readonly char[] indexExprEndChars = new char[] { ']', ')' };
private static readonly char[] indexExprStartChars = new char[] { '[', '(' };
OK,我们先不看代码,就应该知道问题就出在这个地方。当我们分析哪里用到indexExprEndChars时分找到这个方法:
public static object GetIndexedPropertyValue(object container, string expr)
我们不需要阅读里面的代码,通过下面的expr参数注释我们就可以很快得到答案:
上面的注释同时还告诉,我们是可以通过一个对象的导航路径如对象.属性.子属性的方式来绑定一个数据项的间接属性,这个我们可以通过对expressionPartSeparator静态字段的使用,得以验证:
public static object Eval(object container, string expression)
{
if (expression == null)
{
throw new ArgumentNullException("expression");
}
expression = expression.Trim();
if (expression.Length == 0)
{
throw new ArgumentNullException("expression");
}
if (container == null)
{
return null;
}
string[] expressionParts = expression.Split(expressionPartSeparator);
return Eval(container, expressionParts);
}
private static object Eval(object container, string[] expressionParts)
{
object propertyValue = container;
for (int i = 0; (i < expressionParts.Length)&& (propertyValue != null); i++)
{
string propName = expressionParts[i];
if (propName.IndexOfAny(indexExprStartChars)< 0)
{
propertyValue = GetPropertyValue(propertyValue, propName);
}
else
{
propertyValue = GetIndexedPropertyValue(propertyValue, propName);
}
}
return propertyValue;
}
前面的那个Eval重载,把expression表达式用expressionPartSeparator字符分隔开,然后调用内部的 Eval(object,string[])重载,在这个重载中,按顺序去一级一级递归遍历属性值,最终找到最后的那个绑定字段值。所以我们是可以绑定跨级的间接属性和关联DataRowRelation行的值。
还想在再来说说其它的绑定方式,李涛在它的博客浅谈.NET中的数据绑定表达式(二)中提到了绑定数据的七种方式,分别为:
<%#Container.DataItem%>
<%#GetDataItem()%>
<%#Eval("字段名")%>
<%#DataBinder.Eval(Container.DataItem,"字段名")%>
<%#((DataRowView)Container.DataItem)["字段名"] %>
<%#((Type)Container.DataItem).成员 %>
<%#((Type)GetDataItem()).成员 %>
如果按要我来分的话,我只会分成两类:强类型绑定和反射绑定。不论是Container.DataItem还是GetDataItem(),都是得到当前的正在绑定的上下文数据对象,然后转换成他们的原始类型,使用索引或强类型的方式来绑定字段值。而Eval就是使用反射的方式来进行通用化的绑定,这样我们就完全没有必要关心被绑定的数据源是什么类型,在很多场合下这是非常有益的。
从性能方式来考虑,强类型绑定肯定要比反射绑定性能来得好。这其中的原因就不多作解释了,但是对于强类型来说是使用Container.DataItem还是GetDataItem的方式来取得上下文数据对象,性能应该差别不大的。我们在前面已经提到到,在Page的作用域内,会把所有的被绑定(遍历的数据项或整个集合)保存在一个堆栈,方面我们来读取,我们只需要读取堆栈的顶部元素就可以方便的得到当前正在被绑定数据行项;而Container而更像是一个动态的,关键字作用的变量,因为你在绑定不同对象时Container的类型是不一样的,假设你当前正在绑定Repeater那么它的类型是RepeaterItem,只是为了方便我们强类型取得当前Repeater行对象而产生的动态属性,其实它并不是Page的一个公有或私有属性。所以我认为两种取得DataItem的方式在性能上实际是没有多大区别的。
当然我们在选择是使用强类型绑定还是反射绑定时,主要还是取决你的需要。我个人认为,为了使用解决方案通用化,而不必在关心绑定的数据类型是什么类型,应尽量使用Eval的方式来绑定字段。在实践当中,绑定字段的消费上还不是非常多的,为了灵活和通用这点性能损失我认为是值得的。另外就是如上的特殊字段的情况,我当然也可以使用强类型绑定的方式来解决:
<%#((System.Data.DataRowView)Container.DataItem)["电压(a)"]%>
特殊字段的解决之道有很多,比如我们还可以重写Page的Eval方法达到我们的目的。选择哪种方案,就是取决于我们实际需要了。
上面我们从特殊字段名出发,分析了DataBinder在反射取得字段值时所做的一些特殊处理,进而引出我们平常可能会被忽略的一些非常有用的绑定方式,如:索引下标绑定和间接字段绑定,而这些对于我们解决一些疑难问题会有很大的帮助,特别跨级的字段绑定,如果我们没有了解的话,可能就需要在服务器代码中做很多的类型转换和处理。最后我们还讨论了其它的几种绑定方式,以及它们各种的性能和应用场合。
三天,用篇文章来分析了ASP.NET在数据绑定的一个原理,其中很多内容并不是我们平常数据绑定时所需要掌握的知识。但是掌握了它们却对我们在数据绑定时,有更多的把握。正因为内容的动态性,和过于抽象,而本人又无法找到一种最为合适的语言来组织和解释这些知识,代码太多,全部贴出来又感觉找不到重点;贴重要的部分,又感觉跨度太大。所以三篇下来很多要领解释的不是很清楚,大家权当它是一个引子,更多的原理还需要大家自己亲自去分析和阅读代码。