如果希望在同一个单元格中显示多个值,或者希望在单元格中添加 HTML 标签和服务器控件而获得自定义内容的不受限的能力,就需要使用 TemplateField。
TemplateField 允许你为每一列定义一个完全定制的模版。你可以加入控件标签、HTML 元素、数据绑定表达式,可以完全按照你的方式布置一切!
<asp:SqlDataSource ID="sourceEmployees" runat="server" ConnectionString="<%$ConnectionStrings:Northwind %>"
SelectCommand="select * from Employees"></asp:SqlDataSource>
<asp:GridView ID="GridView1" runat="server" DataSourceID="sourceEmployees" AutoGenerateColumns="false"
GridLines="None">
<HeaderStyle BackColor="Silver" />
<Columns>
<asp:TemplateField HeaderText="Employees">
<ItemTemplate>
<b>
<%
1: # Eval("EmployeeID")
%>
-
<%
1: # Eval("TitleOfCourtesy")
%><%
1: # Eval("FirstName")
%><%
1: # Eval("LastName")
%>
</b>
<hr />
<small><i>
<%
1: # Eval("Address")
%><br />
<%
1: # Eval("City")
%>,<%
1: # Eval("Country")
%>,<%
1: # Eval("PostalCode")
%><br />
<%
1: # Eval("HomePhone")
%>
</i>
<br />
<br />
<%
1: # Eval("Notes")
%><br />
<br />
</small>
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
Eval()是 System.Web.UI.DataBinder 类的一个静态方法。
Eval()带来了不可或缺的便利性:
- 自动读取绑定到当前行的数据项,使用发射找到匹配的字段(DataRow 对象)或属性(自定义对象)并获得值
- 反射的过程稍稍增加了一些负载,但几乎可以忽略
如果不使用 Eval()方法,就需要借助 Container.DataItem 属性访问数据对象并像下面这样进行类型转换:
<%# ((System.Data.DataRowView)Container.DataItem)["City"] %>
<%# ((Model.EmployeeDetails)Container.DataItem).FirstName %>
这个方式的问题是,你需要明确知道数据对象的抽象类型。假如使用 ObjectDataSource 绑定了一个对象的数组并使用了上述的类型转换,那么换成了 SqlDataSource 数据源的话,页面代码将无法工作。
而 Eval()只要求对象具有给定名字的属性,数据表达式就可以正常工作,Eval()允许你创建可松散绑定到数据层的页面。
Eval()方法还可以格式化数据字段:<%# Eval("BirthDate","{0:MM/dd/yy}") %>
使用多个模版
ItemTemplate 并非 GridView 提供的唯一模版,在每个模版列里,你都可以使用多个模板来配置外观的各个方面。
HeaderTemplate | 定义表头单元格的外观和内容 |
FooterTemplate | 定义表尾单元格的外观和内容 |
ItemTemplate | 定义数据单元格的外观和内容 |
AlternatingItemTemplate | 和 ItemTemplate 一起格式化奇偶行的外观和容忍 |
EditItemTemplate | 定义编辑模式的外观和使用的控件 |
InsertItemTemplate | 定义插入一条新纪录时的外观和使用的控件 |
EditItemTemplate 很有用,它可以让你具备控制字段编辑的能力。如果不使用模板列,你将只能在普通的文本框编辑且没有任何验证。
模板在 Visual Studio 中是有可视化界面编辑的。
绑定到方法
模版的一个好处是允许使用数据绑定表达式,这大大扩展了格式化和表现绑定数据的方式。在很多场景都会用到的一个关键技术是,在页面类中加入一个专门的方法来处理字段值,这样就突破了简单数据绑定的限制,并允许你加入动态信息和条件逻辑。
<asp:SqlDataSource ID="sourceEmployees" runat="server" ConnectionString="<%$ConnectionStrings:Northwind %>"
SelectCommand="select * from Employees"></asp:SqlDataSource>
<asp:GridView ID="GridView1" runat="server" DataSourceID="sourceEmployees" AutoGenerateColumns="False">
<Columns>
<asp:TemplateField>
<ItemTemplate>
<img alt="City" runat="server" src="<%# GetStatusPicture(Container.DataItem) %>" />
</ItemTemplate>
</asp:TemplateField>
<asp:BoundField DataField="EmployeeID" HeaderText="EmployeeID" />
<asp:BoundField DataField="FirstName" HeaderText="FirstName" />
<asp:BoundField DataField="LastName" HeaderText="LastName" />
<asp:BoundField DataField="Country" HeaderText="Country" />
<asp:BoundField DataField="City" HeaderText="City" />
</Columns>
</asp:GridView>
protected string GetStatusPicture(object obj)
{
// 方式一
//DataRowView view = obj as DataRowView;
//string city = view["City"].ToString();
// 方式二
string city = DataBinder.Eval(obj, "City").ToString();
return city == "London" ? "~/images/Print.ico" : null;
}
提示:
如果使用了数据绑定表达式绑定到方法,将不再可以用回调来优化 GridView 的刷新过程。
如果不愿意牺牲回调的功能,可以不使用模板,而通过 GridView.RowDataBound 事件在项目第一次出现时修改它从而获得相似的功能。
处理模版中的事件
有时候,可能需要处理模板列中的控件所产生的事件:
<asp:TemplateField>
<ItemTemplate>
<asp:ImageButton ID="ImageButton1" runat="server" ImageUrl="<%# GetStatusPicture(Container.DataItem) %>" />
</ItemTemplate>
</asp:TemplateField>
问题是你在模板列中加入一个控件后,GridView 为每个数据项创建一个该控件的一个副本。这样当点击 ImageButton 控件时,你需要通过某种方式确定被单击的图片是哪一个且属于哪一行。
解决这一问题的方法是使用 GridView 的事件而不是按钮事件。GridView.RowCommand 事件就是起这个作用的,因为它在模板中的任意按钮被单击时发生。
模板中的控件事件变成包含控件的容器的事件,这个过程叫做事件冒泡。
当然,还要借助某种方式向 RowCommand 事件传递信息以识别事件究竟发生在哪一行。奥妙在于所有按钮控件的两个字符串属性:CommandName 和 CommandArgument 。你可以为 CommandName 设置一个描述性的名字从而区分当前的单击发生在行内的哪一种按钮上,而 CommandArgument 则可设置一段与行有关的信息以识别点击发生的所在行,这个信息可以通过数据绑定表达式来设置。
<ItemTemplate>
<asp:ImageButton ID="ImageButton1" runat="server" ImageUrl="<%# GetStatusPicture(Container.DataItem) %>"
CommandName="CityClick" CommandArgument='<%# Eval("EmployeeID") %>' />
</ItemTemplate>
protected void GridView1_RowCommand(object sender, GridViewCommandEventArgs e)
{
if (e.CommandName == "CityClick")
{
Label1.Text = "你当前选择的员工ID是: " + e.CommandArgument;
}
}
提示:
你可以直接使用 GridView 内置的对选择的支持(把 CommandName 设置为 Select 并处理 SelectIndexChanged 事件),这样很容易就能访问到被单击的行,但是如果需要提供多个执行不同任务的按钮,它就无能为力了。
使用模版定制编辑模式
标准的编辑模式有以下几个限制:
- 用文本框编辑值并非总是合适的 (有些数据类型用下拉列表更合适,大型字段要用多行文本框)
- 没有验证 (限制编辑输入意外字符的可能性)
- 它通常看起来很丑陋 (网格中遍布文本框的行会占据太多的空间显得很不专业)
使用模板列后就不会有这些烦恼,可以用 EditItemTemplate 定义编辑控件以及它们的布局,虽然这个过程有点费力。
<EditItemTemplate>
<b>
<%# Eval("EmployeeID") %>
-
<%# Eval("TitleOfCourtesy") %><%# Eval("FirstName") %><%# Eval("LastName")%>
</b>
<hr />
<small><i>
<%# Eval("Address") %><br />
<%# Eval("City") %>,<%# Eval("Country") %>,<%# Eval("PostalCode") %><br />
<%# Eval("HomePhone") %>
</i>
<br />
<br />
<asp:TextBox Text='<%# Bind("Notes") %>' runat="server" ID="textBox" TextMode="MultiLine"
Width="413px"></asp:TextBox>
<br />
</small>
</EditItemTemplate>
绑定一个编辑值到控件时,必须在数据绑定表达式中使用 Bind()方法而不是通常的 Eval()方法,只有 Bind()方法才会创建双向链接,更新后的值才能够送回到服务器。因为当 GridView 提交更新时,它只提交被绑定的,可编辑的参数。
提示:
进行增删改操作时,勿忘为 GridView.DataKeyNames 设置主键列!否则更新时将报出异常!
1. 使用高级控件编辑
当你需要绑定到更有意义的控件(如列表控件)时,基于编辑的模版的确很出色。
<EditItemTemplate>
<b>
<%# Eval("EmployeeID") %>
-
<asp:DropDownList ID="DropDownList1" runat="server"
SelectedIndex='<%# GetSelectTitle(Eval("TitleOfCourtesy")) %>'
DataSource="<%# TitleOfCourtesy %>">
</asp:DropDownList>
<%# Eval("FirstName") %>
<%# Eval("LastName")%>
</b>
<hr />
<small><i>
<%# Eval("Address") %><br />
<%# Eval("City") %>,<%# Eval("Country") %>,<%# Eval("PostalCode") %><br />
<%# Eval("HomePhone") %>
</i>
<br />
<br />
<asp:TextBox Text='<%# Bind("Notes") %>' runat="server" ID="textBox" TextMode="MultiLine"
Width="413px"></asp:TextBox>
<br />
</small>
</EditItemTemplate>
这个模版允许用户从一个有限的选项中选择称呼,创建这个列表有一个小技巧 - 把 DropDownList.DataSource 设置为一个指向自定义属性的数据绑定表达式。这个自定义属性返回一个可用称呼的数据源。
protected string[] TitleOfCourtesy
{
get { return new string[] { "Mr.", "Dr.", "Ms.", "Mrs" }; }
}
这一步确保下拉列表被填充,但还没解决在列表中为当前值选定正确称呼的相关问题。最好的做法是把 SelectedIndex 绑定到一个自定义方法上,该方法接受当前称呼并返回称呼的序号:
protected int GetSelectTitle(object title)
{
// 使用 Array 类的静态方法搜索数组返回第一个匹配到的索引
return Array.IndexOf(this.TitleOfCourtesy, title);
}
示例还没有全部完成,现在列表框可以被正确填充并自动匹配到当前编辑行的正确称呼,但如果你改变了选择项,选定的值并不会被回送到数据源。最好的解决办法是处理 RowUpdating 事件,找到当前行的列表控件,解析出文本,然后动态加入额外的参数:
protected void GridView1_RowUpdating(object sender, GridViewUpdateEventArgs e)
{
DropDownList titles = GridView1.Rows[e.RowIndex].FindControl("DropDownList1") as DropDownList;
e.NewValues.Add("TitleOfCourtesy", titles.Text);
}
SqlDataSource 中的 UpdateCommand 通过参数 @TitleOfCourtesy 也会被更新:
<asp:SqlDataSource ID="sourceEmployees" runat="server" ConnectionString="<%$ ConnectionStrings:Northwind %>"
ProviderName="System.Data.SqlClient" SelectCommand="select * from Employees"
UpdateCommand="update Employees set notes=@notes,TitleOfCourtesy=@TitleOfCourtesy where EmployeeID=@EmployeeID"
OnUpdating="sourceEmployees_Updating"></asp:SqlDataSource>
此时已经可以顺利更新 Notes 和 TitleOfCourtest 字段了,你可以看到模版很强大,但编写这样的代码通常不会很快。
2. 无命令列的编辑
目前见到的所有示例都是使用 CommandField 自动生成编辑控件。现在,你已经改用基于模版的方式,值得思考的是应该加入自己的编辑控件。
其实非常简单,只需要在 ItemTemplate 内加入按钮控件,并把它的 CommandName 设置为 Edit,这样按钮就会自动触发编辑流程,产生专门的事件并把行转入编辑模式:
<ItemTemplate>
<b>
<%# Eval("EmployeeID") %>
-
<%# Eval("TitleOfCourtesy") %><%# Eval("FirstName") %><%# Eval("LastName")%>
</b>
<hr />
<small><i>
<%# Eval("Address") %><br />
<%# Eval("City") %>,<%# Eval("Country") %>,<%# Eval("PostalCode") %><br />
<%# Eval("HomePhone") %></i><br />
<br />
<%# Eval("Notes") %><br />
<br />
<asp:LinkButton ID="LinkButton1" runat="server" CommandName="Edit">Edit</asp:LinkButton><br />
<br />
</small>
</ItemTemplate>
编辑模版下当然也需要添加两个按钮用于更新和撤销:
<EditItemTemplate>
<b>
<%# Eval("EmployeeID") %>
-
<asp:DropDownList ID="DropDownList1" runat="server" SelectedIndex='<%# GetSelectTitle(Eval("TitleOfCourtesy")) %>'
DataSource="<%# TitleOfCourtesy %>">
</asp:DropDownList>
<%# Eval("FirstName") %>
<%# Eval("LastName")%>
</b>
<hr />
<small><i>
<%# Eval("Address") %><br />
<%# Eval("City") %>,<%# Eval("Country") %>,<%# Eval("PostalCode") %><br />
<%# Eval("HomePhone") %></i><br />
<br />
<asp:TextBox Text='<%# Bind("Notes") %>' runat="server" ID="textBox" TextMode="MultiLine"
Width="413px"></asp:TextBox>
<br />
<asp:LinkButton ID="LinkButton2" runat="server" CommandName="Update">Update</asp:LinkButton>
<asp:LinkButton ID="LinkButton3" runat="server" CommandName="Cancel">Cancel</asp:LinkButton>
<br />
<br />
</small>
</EditItemTemplate>
只要在任何按钮控件的 CommandName 属性上使用这些名字,GridView 编辑事件就会发生,数据源控件也会以相同的方式响应,这和在使用自动生成的编辑控件时是一致的。
模板中的客户端 ID
ASP.NET 自动为每个重复的元素生成唯一的 ID。但有些时候你需要对客户端进行更多控制,例如通过 JavaScript 修改 HTML 对象。在这种情况下,知道 ASP.NET 要生成的客户端 ID 是什么就非常重要了。
如果把 GridView 的 ClientIDMode 设置为 Predictable,可以用 ClientIDRowSuffix 来选择某个数据字段作为后缀:
<asp:GridView ID="GridView1" runat="server" DataSourceID="sourceEmployees"
ClientIDMode="Predictable" ClientIDRowSuffix="EmployeeID">
现在,每个数据项的 EmployeeID 值被用于后缀,这样确保了客户端 ID 的唯一性。如果某个员工记录的 ID 为 45HJ77,ASP.NET 会生成这样的锚:
<a id="GridView1_cmdEdit_45HJ77" href="javasctipt:_doPostBack(…)">Edit</a>
cmdEdit 是 A 标签的 ID 号