摘要:對於一個以數據處理為主的應用中的UI層,我們往往需要編寫相當多的代碼去實現數據綁定。如果界面上的控制項和作為數據源的實體類型之間存儲某種約定的映射關係,我們就可以實現批量的數據綁定,作者開發了的插件正是用於此,本篇著重介紹如何通過這個組件來解決我們在進行數據綁定過程中的常見問題。 對於一個以數據 ...
摘要:對於一個以數據處理為主的應用中的UI層,我們往往需要編寫相當多的代碼去實現數據綁定。如果界面上的控制項和作為數據源的實體類型之間存儲某種約定的映射關係,我們就可以實現批量的數據綁定,作者開發了的插件正是用於此,本篇著重介紹如何通過這個組件來解決我們在進行數據綁定過程中的常見問題。
對於一個以數據處理為主的應用中的UI層,我們往往需要編寫相當多的代碼去實現數據綁定。如果界面上的控制項和作為數據源的實體類型之間存儲某種約定的映射關係,我們就可以實現批量的數據綁定。為了驗證這種想法,我寫了一個小小的組件。這個小玩意僅僅是我花了兩個小時寫的,其中還有很多問題沒有解決,比如對於空值的處理,特殊控制項屬性值的HTML編碼問題,以及頻繁反射的性能問題,僅僅演示一種解決思路而已。本篇著重介紹如何通過這個組件來解決我們在進行數據綁定過程中的常見問題,下篇會介紹它的設計。[源代碼從這裡下載]
目錄:
一、基於控制項ID/實體屬性名映射的數據綁定
二、一句代碼實現批量數據綁定
三、修正綁定數據的顯示格式
四、過濾不需要綁定的屬性
五、多個控制項對應同一個實體屬性
一、基於控制項ID/實體屬性名映射的數據綁定
我的這個組件暫時命名為DataBinder好了(註意和System.Web.UI.DataBinder區分),我們用它來將一個實體對象綁定給指定的容器控制項中的所有子控制項。下麵是DataBinder的定義,兩個BindData方法實現具體的綁定操作。
public class DataBinder{
public event EventHandler<DataBindingEventArgs> DataItemBinding;
public event EventHandler<DataBindingEventArgs> DataItemBound;
public static IEnumerable<BindingMapping> BuildBindingMappings(Type entityType, Control container, string suffix = "");
public void BindData(object entity, Control container, string suffix = "");
public void BindData( object entity,IEnumerable<BindingMapping> bindingMappings);
}
本文開頭所說,自動批量的數據綁定依賴於控制項和作為數據源實體類型的映射關係。在這裡,我直接採用控制項ID和實體屬性名之間的映射。也就是說,在對於界面上控制項進行命名的時候,應該根據對應的實體類型屬性名進行規範命名。
另一方面,作為數據源的對象來說,它的所有屬性並不都是為數據綁定而涉及。為了讓DataBinder能夠自動篩選用於綁定的屬性,我在相應的屬性上應用了一個自定義特性:DataPropertyAttribute。比如,下麵的Customer對象會在後續的演示中用到,它的每一個數據屬性都應用了這樣一個DataPropertyAttribute特性。
public class Cutomer{
[DataProperty]
public string ID { get; set; }
[DataProperty]
public string FirstName { get; set; }
[DataProperty]
public string LastName { get; set; }
[DataProperty]
public string Gender { get; set; }
[DataProperty]
public int? Age { get; set; }
[DataProperty]
public DateTime? BirthDay { get; set; }
[DataProperty]
public bool? IsVip { get; set; }
}
二、一句代碼實現批量數據綁定
現在我們就來演示如何通過我們定義的DataBinder實現“一句代碼的數據批量綁定”,而作為數據源就是我們上面定義的Customer對象。我們先來設計我們的頁面,下麵是主體部分的HTML,這是一個表格。需要註意的是:所有需要綁定到Customer對象的空間都和對應的屬性具有相同的ID。
<table><tr>
<td style="width:20%;text-align:right">ID:</td>
<td><asp:Label ID="ID" runat="server"></asp:Label></td>
</tr>
<tr>
<td style="width:20%;text-align:right">First Name:</td>
<td><asp:TextBox ID="FirstName" runat="server"></asp:TextBox></td>
</tr>
<tr>
<td style="width:20%;text-align:right">Last Name:</td>
<td><asp:TextBox ID="LastName" runat="server"></asp:TextBox></td>
</tr>
<tr>
<td style="width:20%;text-align:right">Gender:</td>
<td>
<asp:RadioButtonList ID="Gender" runat="server" RepeatDirection="Horizontal">
<asp:ListItem Text="Male" Value = "Male" />
<asp:ListItem Text="Female" Value = "Female" />
</asp:RadioButtonList>
</td>
</tr>
<tr>
<td style="width:20%;text-align:right">Age:</td>
<td><asp:TextBox ID="Age" runat="server"></asp:TextBox></td>
</tr>
<tr>
<td style="width:20%;text-align:right">Birthday:</td>
<td><asp:TextBox ID="Birthday" runat="server" Width="313px"></asp:TextBox></td>
</tr>
<tr>
<td style="width:20%;text-align:right">Is VIP:</td>
<td><asp:CheckBox ID="IsVip" runat="server"></asp:CheckBox></td>
</tr>
<tr>
<td colspan="2" align="center">
<asp:Button ID="ButtonBind" runat="server" Text="Bind" onclick="ButtonBind_Click" />
</td>
</tr>
</table>
為了編成方便,將DataBinder對象作為Page類型的一個屬性,該屬性在構造函數中初始化。
public partial class Default : System.Web.UI.Page{
public Artech.DataBinding.DataBinder DataBinder { get; private set; }
public Default()
{
this.DataBinder = new Artech.DataBinding.DataBinder();
}
}
然後我將數據綁定操作實現的Bind按照的Click事件中,對應所有的代碼如下所示——真正的用於數據綁定的代碼只有一句。
protected void ButtonBind_Click(object sender, EventArgs e){
var customer = new Customer
{
ID = Guid.NewGuid().ToString(),
FirstName = "Zhang",
LastName = "San",
Age = 30,
Gender = "Male",
BirthDay = new DateTime(1981, 1, 1),
IsVip = true
};
this.DataBinder.BindData(customer, this);
}
在瀏覽器中打開該Web頁面,點擊Bind按鈕,你會發現綁定的數據已經正確顯示在了對應的控制項中:
雖然通過DataBinder實現了對多個控制項的批量綁定,但是並不完美。一個顯著的問題是:作為生日的欄位不僅僅顯示了日期,還顯示了時間。我們如何讓日期按照我們要求的格式進行顯示呢?DataBinder為了提供了三種選擇。
如果你註意看DataBinder定義了,你會發現它定義了兩個事件:DataItemBinding和DataItemBound(命名有待商榷),它們分別在對某個控制項進行綁定之前和之後觸發。我們的第一種方案就是註冊DataItemBinding時間,為Birthday指定一個格式化字元串。假設我們需要的格式是“月-日-年”,那麼我們指定的格式化字元串:MM-dd-yyyy。事件註冊我方在了Page的構造函數中:
public Default(){
this.DataBinder = new Artech.DataBinding.DataBinder();
this.DataBinder.DataItemBinding += (sender, args) =>
{
if (args.BindingMapping.Control == this.Birthday)
{
args.BindingMapping.FormatString = "MM-dd-yyyy";
}
};
}
運行程式,你會發現作為生日的欄位已經按照我們希望的格式顯示出來:
上面介紹了通過註冊DataItemBinding事件在綁定前指定格式化字元串的解決方案,你也可以通過註冊DataItemBound事件在綁定後修正顯示的日期格式,相應的代碼如下:
public Default(){
this.DataBinder = new Artech.DataBinding.DataBinder();
this.DataBinder.DataItemBound += (sender, args) =>
{
if (args.BindingMapping.Control == this.Birthday && null != args.DataValue)
{
this.Birthday.Text = ((DateTime)Convert.ChangeType(args.DataValue, typeof(DateTime))).
ToString("MM-dd-yyyy");
}
};
}
DataBinder定義了兩個BindData重載,我們使用的是通過指定數據源和容器控制項的方式,而另一個重載的參數為IEnumerable<BindingMapping>類型。而BindingMapping是我們自定義的類型,用於表示控制項和實體屬性之間的運行時映射關係。而這樣一個BindingMapping集合,可以通過DataBinder的靜態方法BuildBindingMappings來創建。BindingMapping具有一個FormatString表示格式化字元串(實際上面我們指定的格式化字元串就是為這個屬性指定的)。那麼,我們也可以通過下麵的代碼來進行數據綁定:
protected void ButtonBind_Click(object sender, EventArgs e){
var customer = new Customer
{
ID = Guid.NewGuid().ToString(),
FirstName = "Zhang",
LastName = "San",
Age = 30,
Gender = "Male",
BirthDay = new DateTime(1981, 1, 1),
IsVip = true
};
var bindingMappings = Artech.DataBinding.DataBinder.BuildBindingMappings(typeof(Customer), this);
bindingMappings.Where(mapping => mapping.Control == this.Birthday).First().FormatString = "MM-dd-yyyy";
this.DataBinder.BindData(customer, bindingMappings);
}
四、過濾不需要綁定的屬性
在預設的情況下,第一個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:
public Default(){
this.DataBinder = new Artech.DataBinding.DataBinder();
this.DataBinder.DataItemBinding += (sender, args) =>
{
if (args.BindingMapping.Control == this.Birthday)
{
args.BindingMapping.AutomaticBind = false;
}
};
}
程式執行後,Birthday對應的TextBox將不會被綁定:
在上面的例子中,我們的控制項的ID和對應的實體屬性是相同的。但是在很多情況下,相同的頁面上有不止一個控制項映射到實體的同一個屬性上。而控制項ID的唯一性決定了我們不能為它們起相同的ID。在這種情況下,我們採用“基於尾碼”的映射。也就是為,在為控制項進行命名的時候,通過“實體屬性名+尾碼”形式來指定。
如果你仔細看了DataBinder的定義,不論是實例方法BindData(接受Control類型參數的),還是靜態方法BuildBindingMappings,都具有一個預設參數suffix,這就是為這種情況設計的。在預設的情況下,這個參數的值為空字元串,所以我們需要控制項和實體屬性具有相同的名稱。如果控制項是基於“實體屬性名+尾碼”來命名的,就需要顯式指定這個參數了。為了演示這種情況,我們將例子中的所有需要綁定的空間ID加上一個“_Xyz”字元作為尾碼。
<table><tr>
<td style="width:20%;text-align:right">ID:</td>
<td><asp:Label ID="ID_Xyz" runat="server"></asp:Label></td>
</tr>
<tr>
<td style="width:20%;text-align:right">First Name:</td>
<td><asp:TextBox ID="FirstName_Xyz" runat="server"></asp:TextBox></td>
</tr>
<tr>
<td style="width:20%;text-align:right">Last Name:</td>
<td><asp:TextBox ID="LastName_Xyz" runat="server"></asp:TextBox></td>
</tr>
<tr>
<td style="width:20%;text-align:right">Gender:</td>
<td>
<asp:RadioButtonList ID="Gender_Xyz" runat="server" RepeatDirection="Horizontal">
<asp:ListItem Text="Male" Value = "Male" />
<asp:ListItem Text="Female" Value = "Female" />
</asp:RadioButtonList>
</td>
</tr>
<tr>
<td style="width:20%;text-align:right">Age:</td>
<td><asp:TextBox ID="Age_Xyz" runat="server"></asp:TextBox></td>
</tr>
<tr>
<td style="width:20%;text-align:right">Birthday:</td>
<td><asp:TextBox ID="Birthday_Xyz" runat="server" Width="313px"></asp:TextBox></td>
</tr>
<tr>
<td style="width:20%;text-align:right">Is VIP:</td>
<td><asp:CheckBox ID="IsVip_Xyz" runat="server"></asp:CheckBox></td>
</tr>
<tr>
<td colspan="2" align="center">
<asp:Button ID="ButtonBind" runat="server" Text="Bind" onclick="ButtonBind_Click" />
</td>
</tr>
</table>
如果採用指定容器控制項進行直接綁定的話,就可以這樣編程:
protected void ButtonBind_Click(object sender, EventArgs e){
var customer = new Customer
{
ID = Guid.NewGuid().ToString(),
FirstName = "Zhang",
LastName = "San",
Age = 30,
Gender = "Male",
BirthDay = new DateTime(1981, 1, 1),
IsVip = true
};
this.DataBinder.BindData(customer, this, "_Xyz");
}
如果通過預先創建的BindingMapping集合進行數據綁定,那麼代碼將是這樣:
protected void ButtonBind_Click(object sender, EventArgs e){
var customer = new Customer
{
ID = Guid.NewGuid().ToString(),
FirstName = "Zhang",
LastName = "San",
Age = 30,
Gender = "Male",
BirthDay = new DateTime(1981, 1, 1),
IsVip = true
};
var bindingMappings = Artech.DataBinding.DataBinder.BuildBindingMappings(typeof(Customer), this, "_Xyz");
this.DataBinder.BindData(customer, bindingMappings);
}