深入ASP.NET數據綁定
在ASP.NET我們在使用Repeater,DetailsView,FormView,GridView等數據綁定模板時,都會使用<%# Eval("字段名") %>或<%# Bind("字段名") %>這樣的語法來單向或雙向綁定數據。但是我們卻很少去了解,在這些語法的背后,ASP.NET究竟都做了哪些事情來方便我們使用這樣的語法來綁定數據。究竟解析這樣的語法是在編譯時,還是運行時?如果沒有深入去了解,我們肯定不得而知。這個簡短的系列文章就是帶我們大家一起去深入探究一下ASP.NET綁定語法的內部機理,以讓我們更加全面的認識和運用它。
事件的起因是,我希望動態的為Repeater控件添加行項模板,我可以通過實現ITempate接口的方式來動態添加行模板。并希望它通過普通的頁面綁定語法來完成數據字段的綁定功能,如下就是一個簡單的例子:
/// 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綁定代碼:
<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的控件實例:
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函數中,這個函數原型定義是:
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私有函數的定義如下:
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,該函數的定義:
{
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方法,這個方法有兩種重載,簡單起見,我們來分析較為簡單的重載:
{
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的單向數據綁定。