在ASP.NET我们在使用Repeater,DetailsView,FormView,GridView等数据绑定模板时,都会使用<%# Eval("字段名") %>或<%# Bind("字段名") %>这样的语法来单向或双向绑定数据。但是我们却很少去了解,在这些语法的背后,ASP.NET究竟都做了哪些事情来方便我们使用这样的语法来绑定数据。究竟解析这样的语法是在编译时,还是运行时?如果没有深入去了解,我们肯定不得而知。这个简短的系列文章就是带我们大家一起去深入探究一下ASP.NET绑定语法的内部机理,以让我们更加全面的认识和运用它。
事件的起因是,我希望动态的为Repeater控件添加行项模板,我可以通过实现ITempate接口的方式来动态添加行模板。并希望它通过普通的页面绑定语法来完成数据字段的绑定功能,如下就是一个简单的例子:
1: /// <summary>
2: /// Summary description for DynamicTemplate
3: /// </summary>
4: public class DynamicTemplate : ITemplate
5: {
6: public DynamicTemplate()
7: {
8: //
9: // TODO: Add constructor logic here
10: //
11: }
12: #region ITemplate Members
13:
14: public void InstantiateIn(Control container)
15: {
16: TextBox textBox = new TextBox();
17: textBox.Text = @"<%# Eval(""ID"") %>";
18: container.Controls.Add(textBox);
19: }
20: #endregion
21: }
在这个例子中,我在模板中添加了一个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绑定代码:
1: <asp:Repeater runat="server" ID="repeater">
2: <HeaderTemplate>
3: <table>
4: <tr>
5: <td>
6: ID
7: </td>
8: <td>
9: 电流{a}
10: </td>
11: <td>电压(V)</td>
12: <td>
13: 备注'
14: </td>
15: <td>
16: 名称]
17: </td>
18: </tr>
19: </HeaderTemplate>
20: <ItemTemplate>
21: <tr>
22: <td>
23: <%# Eval("ID")%>
24: </td>
25: <td>
26: <%# Eval("电流{a}")%>
27: </td>
28: <td><%# Eval("电压(V)")%></td>
29: <td>
30: <%# Eval("备注'")%>
31: </td>
32: <td>
33: <%# Eval("名称]")%>
34: </td>
35: </tr>
36: </ItemTemplate>
37: <FooterTemplate>
38: </table>
39: </FooterTemplate>
40: </asp:Repeater>
那么在动态类中,相应的会有这样的一段函数,是用来创建ID为repeater的控件实例:
1: [DebuggerNonUserCode]
2: private Repeater __BuildControlrepeater()
3: {
4: Repeater repeater = new Repeater();
5: base.repeater = repeater;
6: repeater.HeaderTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control4));
7: repeater.ItemTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control5));
8: repeater.FooterTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control7));
9: repeater.ID = "repeater";
10: return repeater;
11: }
12:
13:
CompiledTempateBuilder和BuildTemplateMethod只是模板实例化的一个中介,真正用于添加模板内容的是后面的那些私有函数,如ItemTempate的模板内容实例的创建就在__BuildControl__control5函数中,这个函数原型定义是:
1: [DebuggerNonUserCode]
2: private void __BuildControl__control5(Control __ctrl)
3: {
4: DataBoundLiteralControl control = this.__BuildControl__control6();
5: IParserAccessor accessor = __ctrl;
6: accessor.AddParsedSubObject(control);
7: }
8:
在这个函数里,调用了另一个私有函数this.__BuildControl__control6,这个函数返回的一个DataBoundLiteralControl对象,并将对象输出添加到__ctrl参数。事实上,只要我们去阅读CompiledTempateBuilder就发现在,这里的__ctrol对象就是我们在实例化模板时传入的对象,也就是ITemplate中的InstantiateIn方法的那个container参数对象。
为什么使用的是AddParsedSubObject方法,使用这个方法添加子控件相当于告诉父控件,这是一个已经解析好的子控件对象,不需再去将控件解析成HTML代码,而在输出时直接输出Text属性的值即可。从这里我们还可以得知DataBoundLiteralControl的对象,事实上就是承担了字符串拼接的职责,这一点我们可以在后面的分析中得以验证。
__BuildControl__control6私有函数的定义如下:
1: [DebuggerNonUserCode]
2: private DataBoundLiteralControl __BuildControl__control6()
3: {
4: DataBoundLiteralControl control = new DataBoundLiteralControl(5, 4);
5: control.TemplateControl = this;
6: control.SetStaticString(0, "\r\n <tr>\r\n <td>\r\n ");
7: control.SetStaticString(1, "\r\n </td>\r\n <td>\r\n ");
8: control.SetStaticString(2, "\r\n </td>\r\n \r\n <td>\r\n ");
9: control.SetStaticString(3, "\r\n </td>\r\n <td>\r\n ");
10: control.SetStaticString(4, "\r\n </td>\r\n </tr>\r\n ");
11: control.DataBinding += new EventHandler(this.__DataBind__control6);
12: return control;
13: }
在这个函数里面,创建了一个DataBoundLiteralControl对象,并将页面上定义的模板的静态HTML代码添加到该的静态字符串数组里,并且设置了它的绑定事件代理函数__DataBind__control6,该函数的定义:
1: public void __DataBind__control6(object sender, EventArgs e)
2: {
3: DataBoundLiteralControl control = (DataBoundLiteralControl) sender;
4: RepeaterItem bindingContainer = (RepeaterItem) control.BindingContainer;
5: control.SetDataBoundString(0, Convert.ToString(base.Eval("ID"), CultureInfo.CurrentCulture));
6: control.SetDataBoundString(1, Convert.ToString(base.Eval("电流{a}"), CultureInfo.CurrentCulture));
7: control.SetDataBoundString(2, Convert.ToString(base.Eval("备注'"), CultureInfo.CurrentCulture));
8: control.SetDataBoundString(3, Convert.ToString(base.Eval("名称]"), CultureInfo.CurrentCulture));
9: }
在这个函数中,我们看到了真正的数据绑定代码了,它调用了TemplateControl的Eval方法来将当前数据项的相应字段的值取出,并按一定的格式转化后添加到DataBoundLitreralControl对象中,并在DataBoundLiteralControl将StaticString和DataBoundString字符串数组按一定的顺序拼接起来,作为Text属性的输出值。而容器控件则直接向客户端输这段HTML。
下面,我们还有必要来分析下TemplateControl中的Eval方法,这个方法有两种重载,简单起见,我们来分析较为简单的重载:
1: protected internal object Eval(string expression)
2: {
3: this.CheckPageExists();
4: return DataBinder.Eval(this.Page.GetDataItem(), expression);
5: }
这个方法,使用了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的单向数据绑定,在接下来的一篇文章中我们会来探讨ASP.NET通过Bind函数(关键字)
来实现数据双向绑定的机理。