在ASPNET我们在使用RepeaterDetailsViewFormViewGridView等数据绑定模板时都会使用<%# Eval(字段名) %>或<%# Bind(字段名) %>这样的语法来单向或双向绑定数据但是我们却很少去了解在这些语法的背后ASPNET究竟都做了哪些事情来方便我们使用这样的语法来绑定数据究竟解析这样的语法是在编译时还是运行时?如果没有深入去了解我们肯定不得而知这个简短的系列文章就是带我们大家一起去深入探究一下ASPNET绑定语法的内部机理以让我们更加全面的认识和运用它
事件的起因是我希望动态的为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();
: textBoxText = @<%# Eval(ID) %>;
: containerControlsAdd(textBox);
: }
: #endregion
: }
在这个例子中我在模板中添加了一个TextBox控件并指定它的绑定字段是ID但是这做法能否实现我们实现我们需要的功能呢?答案是否定每一行的TextBox的值都是<%# Eval(ID) %>而不会像我们希望的那样去绑定ID字段从结果来分析原因我们可以非常容易得出这段绑定语法并没有得到ASPNET运行时的承认那么页面中使用相同的语法为什么可以呢?故事就是从这里开始的
我们首先要去了解下在页面中使用这样的语法ASPNET都为我们做了哪些事情呢?要了解这个我们要找到aspx文件在首次运行时动态编译的程序集
我们都知道在ASPNET运行时也会把aspx文件编译成一个动态类这个类是继承于aspx的Page指令中Inherits属性指定的类并且同时也直接实现了IHttpHandler接口这个动态类会负责创建页面中使用的各种服务器端控件的实例并且ASPNET运行时会负责解析的编译aspx中存在的服务器端代码(包括绑定语法)并将这些代码编译到这个页面类WebSite工程和Web Application在页面文件上有些不同WebSite工程的每个页面最多可以有两个文件aspx和aspxcs文件而在Web Application还可以包括aspxdesignercs文件这个文件所起的作用也非常有限也就是为了能在页面代码中使用服务器端控件实例而定义的一个实例变量仅此而已所以在设计时WebSite具备更多的动态行为而在运行时WebSite工程和Web Application并没有太大区别
如何得到页面的动态类呢?要首先得到这个页所在的动态程序集在Vista以前的操作系统上一般是在%SystemRoot%\MicrosoftNET\Framework\v\Temporary ASPNET Files 文件夹下而在Vista中而会在%USERPROFILE%\AppData\Local\Temp\Temporary ASPNET 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();
: baserepeater = repeater;
: repeaterHeaderTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this__BuildControl__control));
: repeaterItemTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this__BuildControl__control));
: repeaterFooterTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this__BuildControl__control));
: repeaterID = repeater;
: return repeater;
: }
:
:
CompiledTempateBuilder和BuildTemplateMethod只是模板实例化的一个中介真正用于添加模板内容的是后面的那些私有函数如ItemTempate的模板内容实例的创建就在__BuildControl__control函数中这个函数原型定义是
: [DebuggerNonUserCode]
: private void __BuildControl__control(Control __ctrl)
: {
: DataBoundLiteralControl control = this__BuildControl__control();
: IParserAccessor accessor = __ctrl;
: accessorAddParsedSubObject(control);
: }
:
在这个函数里调用了另一个私有函数this__BuildControl__control这个函数返回的一个DataBoundLiteralControl对象并将对象输出添加到__ctrl参数事实上只要我们去阅读CompiledTempateBuilder就发现在这里的__ctrol对象就是我们在实例化模板时传入的对象也就是ITemplate中的InstantiateIn方法的那个container参数对象
为什么使用的是AddParsedSubObject方法使用这个方法添加子控件相当于告诉父控件这是一个已经解析好的子控件对象不需再去将控件解析成HTML代码而在输出时直接输出Text属性的值即可从这里我们还可以得知DataBoundLiteralControl的对象事实上就是承担了字符串拼接的职责这一点我们可以在后面的分析中得以验证
__BuildControl__control私有函数的定义如下
: [DebuggerNonUserCode]
: private DataBoundLiteralControl __BuildControl__control()
: {
: DataBoundLiteralControl control = new DataBoundLiteralControl( );
: controlTemplateControl = this;
: controlSetStaticString( \r\n <tr>\r\n <td>\r\n );
: controlSetStaticString( \r\n </td>\r\n <td>\r\n );
: controlSetStaticString( \r\n </td>\r\n \r\n <td>\r\n );
: controlSetStaticString( \r\n </td>\r\n <td>\r\n );
: controlSetStaticString( \r\n </td>\r\n </tr>\r\n );
: controlDataBinding += new EventHandler(this__DataBind__control);
: return control;
: }
在这个函数里面创建了一个DataBoundLiteralControl对象并将页面上定义的模板的静态HTML代码添加到该的静态字符串数组里并且设置了它的绑定事件代理函数__DataBind__control该函数的定义
: public void __DataBind__control(object sender EventArgs e)
: {
: DataBoundLiteralControl control = (DataBoundLiteralControl) sender;
: RepeaterItem bindingContainer = (RepeaterItem) controlBindingContainer;
: controlSetDataBoundString( ConvertToString(baseEval(ID) CultureInfoCurrentCulture));
: controlSetDataBoundString( ConvertToString(baseEval(电流{a}) CultureInfoCurrentCulture));
: controlSetDataBoundString( ConvertToString(baseEval(备注) CultureInfoCurrentCulture));
: controlSetDataBoundString( ConvertToString(baseEval(名称]) CultureInfoCurrentCulture));
: }
在这个函数中我们看到了真正的数据绑定代码了它调用了TemplateControl的Eval方法来将当前数据项的相应字段的值取出并按一定的格式转化后添加到DataBoundLitreralControl对象中并在DataBoundLiteralControl将StaticString和DataBoundString字符串数组按一定的顺序拼接起来作为Text属性的输出值而容器控件则直接向客户端输这段HTML
下面我们还有必要来分析下TemplateControl中的Eval方法这个方法有两种重载简单起见我们来分析较为简单的重载
: protected internal object Eval(string expression)
: {
: thisCheckPageExists();
: return DataBinderEval(thisPageGetDataItem() expression);
: }
这个方法使用了DataBinderEval静态方法来得到绑定表达式(字段名)的值它的数据是通过thisPageGetDataItem()这样的一个方法得到的那么为什么thisPageGetDataItem()就可以得到当前正在被绑定的数据项呢?原来在页面绑定数据时它会有一个堆栈来保存它所有的绑定控件绑定时用到的数据项我们只需要取得堆栈顶部的那个元素就可以在页面的作用域内的任何一个位置得到当前正在被绑定的数据项如上的例子我们就可以取得当前绑定的RepeaterItem的DataItem的数据项因此我们不需要与RepeaterItem有任何的联系
如果硬要用上面的代码来描述数据绑定的全过程跨度过大但是有了以上的分析我们再用文字的形式再来总结下应该就会一个比较完整的印象了在ASPNET的数据模板控件中可以使用<%# %>这样的语法来将字段值作为一个占位符用在HTML代码中可以方便我们设计和生成最终的HTML代码不需要很多的字符拼接工作而ASPNET运行时在首次执行页面时会为页面编译一个动态类在这个动态类中会实例化所有的服务器端控件编译和解析绑据模板控件的绑定语法并用一些对象和操作来完成数据绑定的字符串接拼接行为因此绑定语法的解析事实上是编译时的行为只不过这个编译时是延迟到页面的首次执行时这就可以解释为什么在我们想在动态添加模板中使用<%# %>这样的绑定语法时无法解析的原因
而对于DataBinderEval方法这是ASPNET提供的一个数据绑定辅助方法通过这个方法我们可以方便的从种不同的数据项如自定义对象或DataRow取出对象的字段(属性值)从而为我们屏蔽很多不必要的数据来源类型的判断同时DataBinder这个类还提供了其它的绑定辅助方法大家可以从MSDN查看更多有用的帮助
对数据绑定语法的分析就先到此为一个段落在上面我们主要讨论了Eval的单向数据绑定在接下来的一篇文章中我们会来探讨ASPNET通过Bind函数(关键字)
来实现数据双向绑定的机理