一句代碼實現批量數據綁定[上篇]
對於一個以數據處理為主的應用中的UI層,我們往往需要編寫相當多的代碼去實現數據綁定。如果界麵上的控件和作為數據源的實體類型之間存儲某種約定的映射關係,我們就可以實現批量的數據綁定。為了驗證這種想法,我寫了一個小小的組件。這個小玩意僅僅是我花了兩個小時寫的,其中還有很多問題沒有解決,比如對於空值的處理,特殊控件屬性值的HTML編碼問題,以及頻繁反射的性能問題,僅僅演示一種解決思路而已。本篇著重介紹如何通過這個組件來解決我們在進行數據綁定過程中的常見問題,下篇會介紹它的設計。[源代碼從這裏下載]
目錄:
一、基於控件ID/實體屬性名映射的數據綁定
二、一句代碼實現批量數據綁定
三、修正綁定數據的顯示格式
四、過濾不需要綁定的屬性
五、多個控件對應同一個實體屬性
我的這個組件暫時命名為DataBinder好了(注意和System.Web.UI.DataBinder區分),我們用它來將一個實體對象綁定給指定的容器控件中的所有子控件。下麵是DataBinder的定義,兩個BindData方法實現具體的綁定操作。
1: public class DataBinder
2: {
3: public event EventHandler<DataBindingEventArgs> DataItemBinding;
4: public event EventHandler<DataBindingEventArgs> DataItemBound;
5:
6: public static IEnumerable<BindingMapping> BuildBindingMappings(Type entityType, Control container, string suffix = "");
7:
8: public void BindData(object entity, Control container, string suffix = "");
9: public void BindData( object entity,IEnumerable<BindingMapping> bindingMappings);
10: }
本文開頭所說,自動批量的數據綁定依賴於控件和作為數據源實體類型的映射關係。在這裏,我直接采用控件ID和實體屬性名之間的映射。也就是說,在對於界麵上控件進行命名的時候,應該根據對應的實體類型屬性名進行規範命名。
另一方麵,作為數據源的對象來說,它的所有屬性並不都是為數據綁定而涉及。為了讓DataBinder能夠自動篩選用於綁定的屬性,我在相應的屬性上應用了一個自定義特性:DataPropertyAttribute。比如,下麵的Customer對象會在後續的演示中用到,它的每一個數據屬性都應用了這樣一個DataPropertyAttribute特性。
1: public class Customer
2: {
3: [DataProperty]
4: public string ID { get; set; }
5: [DataProperty]
6: public string FirstName { get; set; }
7: [DataProperty]
8: public string LastName { get; set; }
9: [DataProperty]
10: public string Gender { get; set; }
11: [DataProperty]
12: public int? Age { get; set; }
13: [DataProperty]
14: public DateTime? BirthDay { get; set; }
15: [DataProperty]
16: public bool? IsVip { get; set; }
17: }
現在我們就來演示如何通過我們定義的DataBinder實現“一句代碼的數據批量綁定”,而作為數據源就是我們上麵定義的Customer對象。我們先來設計我們的頁麵,下麵是主體部分的HTML,這是一個表格。需要注意的是:所有需要綁定到Customer對象的空間都和對應的屬性具有相同的ID。
1: <table>
2: <tr>
3: <td style="width:20%;text-align:right">ID:</td>
4: <td><asp:Label ID="ID" runat="server"></asp:Label></td>
5: </tr>
6: <tr>
7: <td style="width:20%;text-align:right">First Name:</td>
8: <td><asp:TextBox ID="FirstName" runat="server"></asp:TextBox></td>
9: </tr>
10: <tr>
11: <td style="width:20%;text-align:right">Last Name:</td>
12: <td><asp:TextBox ID="LastName" runat="server"></asp:TextBox></td>
13: </tr>
14: <tr>
15: <td style="width:20%;text-align:right">Gender:</td>
16: <td>
17: <asp:RadioButtonList ID="Gender" runat="server" RepeatDirection="Horizontal">
18: <asp:ListItem Text="Male" Value = "Male" />
19: <asp:ListItem Text="Female" Value = "Female" />
20: </asp:RadioButtonList>
21: </td>
22: </tr>
23: <tr>
24: <td style="width:20%;text-align:right">Age:</td>
25: <td><asp:TextBox ID="Age" runat="server"></asp:TextBox></td>
26: </tr>
27: <tr>
28: <td style="width:20%;text-align:right">Birthday:</td>
29: <td><asp:TextBox ID="Birthday" runat="server" Width="313px"></asp:TextBox></td>
30: </tr>
31: <tr>
32: <td style="width:20%;text-align:right">Is VIP:</td>
33: <td><asp:CheckBox ID="IsVip" runat="server"></asp:CheckBox></td>
34: </tr>
35: <tr>
36: <td colspan="2" align="center">
37: <asp:Button ID="ButtonBind" runat="server" Text="Bind" onclick="ButtonBind_Click" />
38: </td>
39: </tr>
40: </table>
為了編成方便,將DataBinder對象作為Page類型的一個屬性,該屬性在構造函數中初始化。
1: public partial class Default : System.Web.UI.Page
2: {
3: public Artech.DataBinding.DataBinder DataBinder { get; private set; }
4: public Default()
5: {
6: this.DataBinder = new Artech.DataBinding.DataBinder();
7: }
8: }
然後我將數據綁定操作實現的Bind按照的Click事件中,對應所有的代碼如下所示——真正的用於數據綁定的代碼隻有一句。
1: protected void ButtonBind_Click(object sender, EventArgs e)
2: {
3: var customer = new Customer
4: {
5: ID = Guid.NewGuid().ToString(),
6: FirstName = "Zhang",
7: LastName = "San",
8: Age = 30,
9: Gender = "Male",
10: BirthDay = new DateTime(1981, 1, 1),
11: IsVip = true
12: };
13: this.DataBinder.BindData(customer, this);
14: }
在瀏覽器中打開該Web頁麵,點擊Bind按鈕,你會發現綁定的數據已經正確顯示在了對應的控件中:
雖然通過DataBinder實現了對多個控件的批量綁定,但是並不完美。一個顯著的問題是:作為生日的字段不僅僅顯示了日期,還顯示了時間。我們如何讓日期按照我們要求的格式進行顯示呢?DataBinder為了提供了三種選擇。
如果你注意看DataBinder定義了,你會發現它定義了兩個事件:DataItemBinding和DataItemBound(命名有待商榷),它們分別在對某個控件進行綁定之前和之後觸發。我們的第一種方案就是注冊DataItemBinding時間,為Birthday指定一個格式化字符串。假設我們需要的格式是“月-日-年”,那麼我們指定的格式化字符串:MM-dd-yyyy。事件注冊我方在了Page的構造函數中:
1: public Default()
2: {
3: this.DataBinder = new Artech.DataBinding.DataBinder();
4: this.DataBinder.DataItemBinding += (sender, args) =>
5: {
6: if (args.BindingMapping.Control == this.Birthday)
7: {
8: args.BindingMapping.FormatString = "MM-dd-yyyy";
9: }
10: };
11: }
運行程序,你會發現作為生日的字段已經按照我們希望的格式顯示出來:
上麵介紹了通過注冊DataItemBinding事件在綁定前指定格式化字符串的解決方案,你也可以通過注冊DataItemBound事件在綁定後修正顯示的日期格式,相應的代碼如下:
1: public Default()
2: {
3: this.DataBinder = new Artech.DataBinding.DataBinder();
4: this.DataBinder.DataItemBound += (sender, args) =>
5: {
6: if (args.BindingMapping.Control == this.Birthday && null != args.DataValue)
7: {
8: this.Birthday.Text = ((DateTime)Convert.ChangeType(args.DataValue, typeof(DateTime))).
9: ToString("MM-dd-yyyy");
10: }
11: };
12: }
DataBinder定義了兩個BindData重載,我們使用的是通過指定數據源和容器控件的方式,而另一個重載的參數為IEnumerable<BindingMapping>類型。而BindingMapping是我們自定義的類型,用於表示控件和實體屬性之間的運行時映射關係。而這樣一個BindingMapping集合,可以通過DataBinder的靜態方法BuildBindingMappings來創建。BindingMapping具有一個FormatString表示格式化字符串(實際上麵我們指定的格式化字符串就是為這個屬性指定的)。那麼,我們也可以通過下麵的代碼來進行數據綁定:
1: protected void ButtonBind_Click(object sender, EventArgs e)
2: {
3: var customer = new Customer
4: {
5: ID = Guid.NewGuid().ToString(),
6: FirstName = "Zhang",
7: LastName = "San",
8: Age = 30,
9: Gender = "Male",
10: BirthDay = new DateTime(1981, 1, 1),
11: IsVip = true
12: };
13: var bindingMappings = Artech.DataBinding.DataBinder.BuildBindingMappings(typeof(Customer), this);
14: bindingMappings.Where(mapping => mapping.Control == this.Birthday).First().FormatString = "MM-dd-yyyy";
15: this.DataBinder.BindData(customer, bindingMappings);
16: }
在默認的情況下,第一個BindData方法(指定容器控件)會遍曆實體的所有屬性,將其綁定到對應的控件上。可能在有的時候,對於某些特殊的屬性,我們不需要進行綁定。比如,某個控件的ID雖然符合實體屬性的映射,但是它們表示的其實根本不是相同性質的數據。
為了解決在這個問題,在BindingMapping類型中定義了一個布爾類型的AutomaticBind屬性。如果你在綁定前將該屬性設置成False,那麼基於該BindingMapping的數據綁定將被忽略。如果你調用BindData(object entity, Control container, string suffix = "")這個重載,你可以通過注冊DataItemBinding事件將相應BindingMapping的AutomaticBind屬性設置成False。如果你調用BindData( object entity,IEnumerable<BindingMapping> bindingMappings)這個重載,你隻需要在調用之間將相應BindingMapping的AutomaticBind屬性設置成False。
我們將我們的程序還原成最初的狀態,現在通過注冊BindingMapping事件將基於Birthday的BindingMapping的AutomaticBind屬性設置成False:
1: public Default()
2: {
3: this.DataBinder = new Artech.DataBinding.DataBinder();
4: this.DataBinder.DataItemBinding += (sender, args) =>
5: {
6: if (args.BindingMapping.Control == this.Birthday)
7: {
8: args.BindingMapping.AutomaticBind = false;
9: }
10: };
11: }
程序執行後,Birthday對應的TextBox將不會被綁定:
在上麵的例子中,我們的控件的ID和對應的實體屬性是相同的。但是在很多情況下,相同的頁麵上有不止一個控件映射到實體的同一個屬性上。而控件ID的唯一性決定了我們不能為它們起相同的ID。在這種情況下,我們采用“基於後綴”的映射。也就是為,在為控件進行命名的時候,通過“實體屬性名+後綴”形式來指定。
如果你仔細看了DataBinder的定義,不論是實例方法BindData(接受Control類型參數的),還是靜態方法BuildBindingMappings,都具有一個缺省參數suffix,這就是為這種情況設計的。在默認的情況下,這個參數的值為空字符串,所以我們需要控件和實體屬性具有相同的名稱。如果控件是基於“實體屬性名+後綴”來命名的,就需要顯式指定這個參數了。為了演示這種情況,我們將例子中的所有需要綁定的空間ID加上一個“_Xyz”字符作為後綴。
1: <table>
2: <tr>
3: <td style="width:20%;text-align:right">ID:</td>
4: <td><asp:Label ID="ID_Xyz" runat="server"></asp:Label></td>
5: </tr>
6: <tr>
7: <td style="width:20%;text-align:right">First Name:</td>
8: <td><asp:TextBox ID="FirstName_Xyz" runat="server"></asp:TextBox></td>
9: </tr>
10: <tr>
11: <td style="width:20%;text-align:right">Last Name:</td>
12: <td><asp:TextBox ID="LastName_Xyz" runat="server"></asp:TextBox></td>
13: </tr>
14: <tr>
15: <td style="width:20%;text-align:right">Gender:</td>
16: <td>
17: <asp:RadioButtonList ID="Gender_Xyz" runat="server" RepeatDirection="Horizontal">
18: <asp:ListItem Text="Male" Value = "Male" />
19: <asp:ListItem Text="Female" Value = "Female" />
20: </asp:RadioButtonList>
21: </td>
22: </tr>
23: <tr>
24: <td style="width:20%;text-align:right">Age:</td>
25: <td><asp:TextBox ID="Age_Xyz" runat="server"></asp:TextBox></td>
26: </tr>
27: <tr>
28: <td style="width:20%;text-align:right">Birthday:</td>
29: <td><asp:TextBox ID="Birthday_Xyz" runat="server" Width="313px"></asp:TextBox></td>
30: </tr>
31: <tr>
32: <td style="width:20%;text-align:right">Is VIP:</td>
33: <td><asp:CheckBox ID="IsVip_Xyz" runat="server"></asp:CheckBox></td>
34: </tr>
35: <tr>
36: <td colspan="2" align="center">
37: <asp:Button ID="ButtonBind" runat="server" Text="Bind" onclick="ButtonBind_Click" />
38: </td>
39: </tr>
40: </table>
如果采用指定容器控件進行直接綁定的話,就可以這樣編程:
1: protected void ButtonBind_Click(object sender, EventArgs e)
2: {
3: var customer = new Customer
4: {
5: ID = Guid.NewGuid().ToString(),
6: FirstName = "Zhang",
7: LastName = "San",
8: Age = 30,
9: Gender = "Male",
10: BirthDay = new DateTime(1981, 1, 1),
11: IsVip = true
12: };
13: this.DataBinder.BindData(customer, this, "_Xyz");
14: }
如果通過預先創建的BindingMapping集合進行數據綁定,那麼代碼將是這樣:
1: protected void ButtonBind_Click(object sender, EventArgs e)
2: {
3: var customer = new Customer
4: {
5: ID = Guid.NewGuid().ToString(),
6: FirstName = "Zhang",
7: LastName = "San",
8: Age = 30,
9: Gender = "Male",
10: BirthDay = new DateTime(1981, 1, 1),
11: IsVip = true
12: };
13:
14: var bindingMappings = Artech.DataBinding.DataBinder.BuildBindingMappings(typeof(Customer), this, "_Xyz");
15: this.DataBinder.BindData(customer, bindingMappings);
16: }
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-27 11:34:15