ListView提示和技巧
2008-06-05 15:20 Valens 阅读(1287) 评论(0) 编辑 收藏 举报 概括来说,ListView 是 DataList 控件的增强版本,它提供了对生成标记的更多控制,还支持分页功能,并与基于数据源的绑定模型实现了全面集成。
如您所见,它包含一个顶级字符串,后跟一组链接列表。您可以使用第一个 ListView 创建 H1 元素,然后使用嵌套的 ListView(或类似的数据绑定控件)来呈现链接列表。在第一步中,您需要获取数据来填充菜单。理想情况下,您应该使用以下伪类型对象的集合来生成每个项目:
下面说明如何使用 LINQ to XML 来加载和处理内容:
加载文档之后,选择名为 RightMenuItems 的第一个节点,然后获取它的所有 MenuItem 子项。各个 MenuItem 节点的内容都被加载到一个新的匿名类型中,此类型包含两个属性——Title 和 Links。应该如何来填充 Links 集合呢?部分代码如下所示:
下一步是将此复合数据绑定到用户界面。如前文所述,您要使用外部 ListView 来呈现标题,使用第二个嵌套的 ListView 来呈现子链接列表(请参见图 2)。请注意,最内侧的 ListView 必须使用 Eval 方法绑定到数据——任何其他方法都不会起作用:
在本例中,提取客户数据时,只会预取于 1997 年发出的订单。当需要预取相关数据以及需要应用筛选器时,可以使用 AssociateWith 方法。您必须自行确保表之间不存在循环引用(例如,当您为某个客户加载订单然后又为某个订单加载客户时),如下所示:
现在已经准备好了所有数据,接下来就可以考虑进行绑定了。在本例中,一个二级 ListView 控件就能很好地完成此任务。将顶级 ListView 绑定到 Customer 对象集合,然后将最内侧的 ListView 绑定到各个已绑定 Customer 对象的 Orders 属性。图 4 中的代码显示了用于三级分层视图的标记,在该视图中客户显示在第一级,并由最外层 ListView 的 ItemTemplate 属性来呈现。然后将嵌入的 ListView 绑定到订单。最后,嵌入 ListView 的 ItemTemplate 将包含一个 GridView,其中列出了每份订单的明细。
需要对图 4 中的代码进行少量改动,以支持可折叠的面板扩展器。特别地,您应该编辑名为 panelCustomerInfo 的面板,以添加用于展开和折叠子视图的按钮。下面是重新编写面板标记的一种方法:
该按钮与客户名称位于同一行,使用右对齐图像来呈现。扩展器的 TargetControlID 属性引用页面中将要折叠和展开的面板。此面板就是包含订单和订单明细的那个面板。如您在图 4 中所见,它是名为 panelCustomerDetails 的面板。
作为此操作的替代方法,您可以使用 ObjectDataSource 控件来提供数据并实现任何分页逻辑。然后,在业务对象中,可以使用 LINQ to SQL 或纯 ADO.NET 对数据进行访问。
接着,我将 DataPager 移动到同一个母版的另一个内容区域中。出乎意料的是,DataPager 竟然无法与 ListView 控件通信。出现这个问题的原因在于 DataPager 控件定位分页控件时所使用的算法。如果两个控件位于不同的命名容器中,则此算法将无法正常工作。为解决此问题,您需要使用分页控件的完整、唯一 ID(其中包括命名容器信息)来标识控件。遗憾的是,您无法通过声明的方式简单地设置此信息。
在代码中,您可以采用如下方式来处理事件:
在本专栏中,我将深入介绍 ListView 模板和数据绑定的基础知识,以实现在实际页面中非常常见但却需要额外编码的一些功能。您将了解到如何使用嵌套的 ListView 控件来创建数据的分层视图,以及如何通过派生自定义 ListView 类来扩展 ListView 事件模型。
特别是,我将改进事件模型,以便您能够为不同的绑定数据项组使用不同的模板。例如,您可以对数据集中与给定标准匹配的所有数据项使用不同的模板。这绝不仅仅是简单地将特定项目设置为不同的样式;您可以在任何视图控件中仅通过处理 ItemDataBound 事件即可轻松完成此任务。
通常,菜单是由一系列使用 CSS 设计的 <li> 标记实现的。呈现平面菜单不会引起任何特殊的绑定问题,但如果需要一个或多个子菜单时会发生什么情况呢?在这种情况下,您可以使用内置的菜单控件,也可以借助 ListView 来创建极具个性化的呈现方式。顺便说一下,要注意在默认情况下,菜单控件使用基于表格的输出,这与 ListView 所提供的 CSS 友好输出截然不同。(要使菜单控件具有 CSS 友好输出,您需要安装并配置“CSS 控件适配器”工具包,它可以从 http://www.asp.ne/ 下载。)
构建分层菜单
许多 Web 应用程序都在页面的左侧或右侧提供了垂直菜单。利用此菜单,用户能够导航至二级或多级嵌套页面。在这里 ASP.NET 菜单控件无疑是一种可行的选择。但是,我更倾向于仅当菜单需要使用分层数据源(通常为 XML 文件)以及需要创建飞出式子菜单时才使用菜单控件。
对于静态的多级项目列表,我选择使用 repeater 型控件来输出 UI 设计团队创建的标记。在 ASP.NET 3.5 中,可供选择的 repeater 型控件是 ListView 控件。
假设有一个类似于图 1 所示的菜单。它显示在 CoffeeNCream 免费 HTML 模板中,此模板可从 oswd.org 下载。在示例页面中,我只是简单地将 HTML 标记并入到了 ASP.NET 母版页中。
Figure 1 A Standard Menu
右侧菜单项的 HTML 源代码类似于下方所示:
<h1>Something</h1>
<ul>
<li><a href="#">pellentesque</a></li>
<li><a href="#">sociis natoque</a></li>
<li><a href="#">semper</a></li>
<li><a href="#">convallis</a></li>
</ul>
<ul>
<li><a href="#">pellentesque</a></li>
<li><a href="#">sociis natoque</a></li>
<li><a href="#">semper</a></li>
<li><a href="#">convallis</a></li>
</ul>
如您所见,它包含一个顶级字符串,后跟一组链接列表。您可以使用第一个 ListView 创建 H1 元素,然后使用嵌套的 ListView(或类似的数据绑定控件)来呈现链接列表。在第一步中,您需要获取数据来填充菜单。理想情况下,您应该使用以下伪类型对象的集合来生成每个项目:
class MenuItem {
public string Title;
public Collection<Link> Links;
}
class Link {
public string Url;
public string Text;
}
public string Title;
public Collection<Link> Links;
}
class Link {
public string Url;
public string Text;
}
填充 MenuItem 集合的合理方法是从 XML 文件中呈现信息。下面是该文档的一种可能架构:
<Data>
<RightMenuItems>
<MenuItem>
<Title>Something</Title>
<Link url="" text="pellentesque" />
:
</MenuItem>
</RightMenuItems>
</Data>
<RightMenuItems>
<MenuItem>
<Title>Something</Title>
<Link url="" text="pellentesque" />
:
</MenuItem>
</RightMenuItems>
</Data>
下面说明如何使用 LINQ to XML 来加载和处理内容:
var doc = XDocument.Load(Server.MapPath("dataMap.xml"));
var menu = (from e in doc.Descendants("RightMenuItems")
select e).First();
var menuLinks = from mi in menu.Descendants("MenuItem")
select new
{
Title = mi.Value,
Links = ()
};
var menu = (from e in doc.Descendants("RightMenuItems")
select e).First();
var menuLinks = from mi in menu.Descendants("MenuItem")
select new
{
Title = mi.Value,
Links = ()
};
加载文档之后,选择名为 RightMenuItems 的第一个节点,然后获取它的所有 MenuItem 子项。各个 MenuItem 节点的内容都被加载到一个新的匿名类型中,此类型包含两个属性——Title 和 Links。应该如何来填充 Links 集合呢?部分代码如下所示:
Links = (from l in mi.Descendants("Link")
select new {Url=l.Attribute("url").Value,
Text=l.Attribute("text").Value})
select new {Url=l.Attribute("url").Value,
Text=l.Attribute("text").Value})
下一步是将此复合数据绑定到用户界面。如前文所述,您要使用外部 ListView 来呈现标题,使用第二个嵌套的 ListView 来呈现子链接列表(请参见图 2)。请注意,最内侧的 ListView 必须使用 Eval 方法绑定到数据——任何其他方法都不会起作用:
Figure 2 嵌套的 ListView
<asp:ListView runat="server" ID="RightMenuItems"
ItemPlaceholderID="PlaceHolder2">
<LayoutTemplate>
<asp:PlaceHolder runat="server" ID="PlaceHolder2" />
</LayoutTemplate>
<ItemTemplate>
<h1><%# Eval("Title") %></h1>
<asp:ListView runat="server" ID="subMenu"
ItemPlaceholderID="PlaceHolder3"
DataSource='<%# Eval("Links") %>'>
<LayoutTemplate>
<ul>
<asp:PlaceHolder runat="server" ID="PlaceHolder3" />
</ul>
</LayoutTemplate>
<ItemTemplate>
<li>
<a href='<%# Eval("Url") %>'><%# Eval("Text") %></a>
</li>
</ItemTemplate>
</asp:ListView>
</ItemTemplate>
</asp:ListView>
ItemPlaceholderID="PlaceHolder2">
<LayoutTemplate>
<asp:PlaceHolder runat="server" ID="PlaceHolder2" />
</LayoutTemplate>
<ItemTemplate>
<h1><%# Eval("Title") %></h1>
<asp:ListView runat="server" ID="subMenu"
ItemPlaceholderID="PlaceHolder3"
DataSource='<%# Eval("Links") %>'>
<LayoutTemplate>
<ul>
<asp:PlaceHolder runat="server" ID="PlaceHolder3" />
</ul>
</LayoutTemplate>
<ItemTemplate>
<li>
<a href='<%# Eval("Url") %>'><%# Eval("Text") %></a>
</li>
</ItemTemplate>
</asp:ListView>
</ItemTemplate>
</asp:ListView>
<asp:ListView runat="server" ID="subMenu"
ItemPlaceholderID="PlaceHolder3"
DataSource='<%# Eval("Links") %>'>
</asp:ListView>
ItemPlaceholderID="PlaceHolder3"
DataSource='<%# Eval("Links") %>'>
</asp:ListView>
通过将数据连接到顶级 ListView 来启动数据绑定过程。此时,ListView 的主体将完整地呈现出来,包括嵌套的 ListView。理论上说,您可以截取父 ListView 的 ItemDataBound 事件、遍历控件树、获取对子 ListView 的引用,然后以编程方式将其绑定到数据。如果这样做,将不会抛出异常,但内部 ListView 的绑定命令会丢失,因为它触发的时间太晚,无法影响呈现。另一方面,在任何数据绑定事件期间,都会在控件生命周期的合适时间自动计算一个数据绑定表达式。这样可以确保将正确的数据正确地绑定到用户界面。
创建分层视图
可以采用与填充分层菜单相同的模型来构建任何分层数据视图。在本例中,替代选项是使用 TreeView 控件来实现数据的多级表示。但是 TreeView 控件上的数据绑定要求使用分层数据源。在设计数据源结构和最终用户界面时,使用嵌套的 ListView 控件可以为您提供更大的灵活性。下面我们将详述这些概念。
假设您需要创建一个分层数据网格,并根据现有表关系在其中显示客户、订单和订单明细等信息。您应该如何检索数据并将其绑定到控件呢?请看一下图 3 中的代码。您可以使用 LINQ to SQL 轻松地将数据加载到用于包含数据分层的对象模型中。请注意,在 LINQ to SQL 中运行查询时,您实际上仅检索那些显式请求的数据。换句话说,它仅提取图表的第一级,而不会同时自动加载任何相关的对象。
Figure 3 加载正确的数据
Public Class DataCache
{
public IEnumerable GetCustomers()
{
NorthwindDataContext db = new NorthwindDataContext();
DataLoadOptions opt = new DataLoadOptions();
opt.LoadWith<Customer>(c => c.Orders);
opt.LoadWith<Order>(o => o.Order_Details);
db.LoadOptions = opt;
var data = from c in db.Customers
select new { c.CompanyName, c.Orders };
return data.ToList();
}
public int GetCustomersCount()
{
// Return the number of customers
NorthwindDataContext db = new NorthwindDataContext();
return db.Customers.Count();
}
public IEnumerable GetCustomers(int maxRows, int startRowIndex)
{
if (maxRows < 0)
return GetCustomers();
NorthwindDataContext db = new NorthwindDataContext();
DataLoadOptions opt = new DataLoadOptions();
opt.LoadWith<Customer>(c => c.Orders);
opt.LoadWith<Order>(o => o.Order_Details);
db.LoadOptions = opt;
var data = (from c in db.Customers
select new {
c.CompanyName,
c.Orders
}).Skip(startRowIndex).Take(maxRows);
return data.ToList();
}
}
NorthwindDataContext db = new NorthwindDataContext();
DataLoadOptions opt = new DataLoadOptions();
opt.LoadWith<Customer>(c => c.Orders);
opt.LoadWith<Order>(o => o.Order_Details);
db.LoadOptions = opt;
{
public IEnumerable GetCustomers()
{
NorthwindDataContext db = new NorthwindDataContext();
DataLoadOptions opt = new DataLoadOptions();
opt.LoadWith<Customer>(c => c.Orders);
opt.LoadWith<Order>(o => o.Order_Details);
db.LoadOptions = opt;
var data = from c in db.Customers
select new { c.CompanyName, c.Orders };
return data.ToList();
}
public int GetCustomersCount()
{
// Return the number of customers
NorthwindDataContext db = new NorthwindDataContext();
return db.Customers.Count();
}
public IEnumerable GetCustomers(int maxRows, int startRowIndex)
{
if (maxRows < 0)
return GetCustomers();
NorthwindDataContext db = new NorthwindDataContext();
DataLoadOptions opt = new DataLoadOptions();
opt.LoadWith<Customer>(c => c.Orders);
opt.LoadWith<Order>(o => o.Order_Details);
db.LoadOptions = opt;
var data = (from c in db.Customers
select new {
c.CompanyName,
c.Orders
}).Skip(startRowIndex).Take(maxRows);
return data.ToList();
}
}
NorthwindDataContext db = new NorthwindDataContext();
DataLoadOptions opt = new DataLoadOptions();
opt.LoadWith<Customer>(c => c.Orders);
opt.LoadWith<Order>(o => o.Order_Details);
db.LoadOptions = opt;
DataLoadOptions 类可用于修改 LINQ to SQL 引擎的默认行为,因此可以立即加载特定关系所引用的数据。图 3 中的代码用来确保订单随客户一同加载,订单明细随订单一同加载。
LoadWith 方法根据指定关系加载数据。随后可使用 AssociateWith 方法筛选相关的预取对象,如下所示:
opt.AssociateWith<Customer>(
c => c.Orders.Where(o => o.OrderDate.Value.Year == 1997));
c => c.Orders.Where(o => o.OrderDate.Value.Year == 1997));
在本例中,提取客户数据时,只会预取于 1997 年发出的订单。当需要预取相关数据以及需要应用筛选器时,可以使用 AssociateWith 方法。您必须自行确保表之间不存在循环引用(例如,当您为某个客户加载订单然后又为某个订单加载客户时),如下所示:
DataLoadOptions opt = new DataLoadOptions();
opt.LoadWith<Customer> (c => c.Orders);
opt.LoadWith<Order> (o => o.Customer);
opt.LoadWith<Customer> (c => c.Orders);
opt.LoadWith<Order> (o => o.Customer);
现在已经准备好了所有数据,接下来就可以考虑进行绑定了。在本例中,一个二级 ListView 控件就能很好地完成此任务。将顶级 ListView 绑定到 Customer 对象集合,然后将最内侧的 ListView 绑定到各个已绑定 Customer 对象的 Orders 属性。图 4 中的代码显示了用于三级分层视图的标记,在该视图中客户显示在第一级,并由最外层 ListView 的 ItemTemplate 属性来呈现。然后将嵌入的 ListView 绑定到订单。最后,嵌入 ListView 的 ItemTemplate 将包含一个 GridView,其中列出了每份订单的明细。
Figure 4 三级分层结构
<asp:ListView ID="ListView1" runat="server"
DataSourceID="ObjectDataSource1"
ItemPlaceholderID="lvItemPlaceHolder">
<LayoutTemplate>
<asp:PlaceHolder runat="server" ID="lvItemPlaceHolder" />
</LayoutTemplate>
<ItemTemplate>
<asp:Panel runat="server" ID="panelCustomerInfo"
cssclass="customerInfo">
<%# Eval("CompanyName") %>
</asp:Panel>
<asp:panel runat="server" ID="panelCustomerDetails"
cssclass="customerDetails">
<asp:ListView runat="server"
DataSource='<%# Eval("Orders") %>'
ItemPlaceholderID="lvOrdersItemPlaceHolder">
<LayoutTemplate>
<ul>
<asp:PlaceHolder runat="server"
ID="lvOrdersItemPlaceHolder" />
</ul>
</LayoutTemplate>
<ItemTemplate>
<li>
Order #<%# Eval("OrderID") %>
<span class="orderDate">
placed on <%#
((DateTime)Eval("OrderDate")).ToString
("ddd, dd MMM yyyy") %>
</span>
<span class="orderEmployee">
managed by <b><%# Eval("Employee.LastName") %></b>
</span>
<asp:GridView runat="server"
DataSource='<%# Eval("Order_Details") %>'
SkinID="OrderDetailsGridSkin" >
</asp:GridView>
</li>
</ItemTemplate>
</asp:ListView>
</asp:panel>
</ItemTemplate>
</asp:ListView>
DataSourceID="ObjectDataSource1"
ItemPlaceholderID="lvItemPlaceHolder">
<LayoutTemplate>
<asp:PlaceHolder runat="server" ID="lvItemPlaceHolder" />
</LayoutTemplate>
<ItemTemplate>
<asp:Panel runat="server" ID="panelCustomerInfo"
cssclass="customerInfo">
<%# Eval("CompanyName") %>
</asp:Panel>
<asp:panel runat="server" ID="panelCustomerDetails"
cssclass="customerDetails">
<asp:ListView runat="server"
DataSource='<%# Eval("Orders") %>'
ItemPlaceholderID="lvOrdersItemPlaceHolder">
<LayoutTemplate>
<ul>
<asp:PlaceHolder runat="server"
ID="lvOrdersItemPlaceHolder" />
</ul>
</LayoutTemplate>
<ItemTemplate>
<li>
Order #<%# Eval("OrderID") %>
<span class="orderDate">
placed on <%#
((DateTime)Eval("OrderDate")).ToString
("ddd, dd MMM yyyy") %>
</span>
<span class="orderEmployee">
managed by <b><%# Eval("Employee.LastName") %></b>
</span>
<asp:GridView runat="server"
DataSource='<%# Eval("Order_Details") %>'
SkinID="OrderDetailsGridSkin" >
</asp:GridView>
</li>
</ItemTemplate>
</asp:ListView>
</asp:panel>
</ItemTemplate>
</asp:ListView>
使用扩展器改进用户体验
坦白地讲,通过图 4 中的代码得到的用户界面并不怎么吸引人。因为现在正在构建分层数据视图,所以使用展开/折叠面板对改进用户体验而言是最适合的解决方案。ASP.NET AJAX 控件工具包提供了一个现成的扩展器,当应用到面板服务器控件时,它可以为与各个客户和订单相关联的信息加入下拉列表效果。
使用 CollapsiblePanelExtender 控件在页面控件树中定义一个面板,此面板的展开和折叠将通过脚本来控制。不用说,作为页面开发人员,您不需要编写任何 JavaScript。展开和折叠面板所需的所有脚本都通过扩展器控件自动注入。让我们来看一下您可能希望在扩展器中设置的属性:
<act:CollapsiblePanelExtender runat="server" ID="CollapsiblePanel1"
TargetControlID="panelCustomerDetails"
Collapsed="true"
ScrollContents="true"
SuppressPostback="true"
ExpandedSize="250px"
ImageControlID="Image1"
ExpandedImage="~/images/collapse.jpg"
CollapsedImage="~/images/expand.jpg"
ExpandControlID="Image1"
CollapseControlID="Image1">
</act:CollapsiblePanelExtender>
TargetControlID="panelCustomerDetails"
Collapsed="true"
ScrollContents="true"
SuppressPostback="true"
ExpandedSize="250px"
ImageControlID="Image1"
ExpandedImage="~/images/collapse.jpg"
CollapsedImage="~/images/expand.jpg"
ExpandControlID="Image1"
CollapseControlID="Image1">
</act:CollapsiblePanelExtender>
需要对图 4 中的代码进行少量改动,以支持可折叠的面板扩展器。特别地,您应该编辑名为 panelCustomerInfo 的面板,以添加用于展开和折叠子视图的按钮。下面是重新编写面板标记的一种方法:
<asp:Panel ID="panelCustomerInfo" runat="server">
<div class="customerInfo">
<div style="float: left;"><%# Eval("CompanyName") %></div>
<div style="float: right; vertical-align: middle;">
<asp:ImageButton ID="Image1" runat="server"
ImageUrl="~/images/expand.jpg"
AlternateText="(Show Orders)"/>
</div>
</div>
</asp:Panel>
<div class="customerInfo">
<div style="float: left;"><%# Eval("CompanyName") %></div>
<div style="float: right; vertical-align: middle;">
<asp:ImageButton ID="Image1" runat="server"
ImageUrl="~/images/expand.jpg"
AlternateText="(Show Orders)"/>
</div>
</div>
</asp:Panel>
该按钮与客户名称位于同一行,使用右对齐图像来呈现。扩展器的 TargetControlID 属性引用页面中将要折叠和展开的面板。此面板就是包含订单和订单明细的那个面板。如您在图 4 中所见,它是名为 panelCustomerDetails 的面板。
ExpandControlID 和 CollapseControlID 属性指明单击、展开和折叠目标面板时所用元素的 ID。如果计划使用不同的图像来反映面板的状态,则还需要指定图像控件的 ID。此信息属于 ImageControlID 属性。ImageControlID 与另外两个用来保存图像 URL 的属性(CollapsedImage 和 ExpandedImage)相关联。
ExpandedSize 属性以像素为单位设置展开面板允许的最大高度。默认情况下,超出最大高度的所有内容都将被切除。但是,如果将 ScrollContents 属性设置为 true,则会添加一个垂直滚动条,允许用户滚动浏览所有内容。
最后,Collapsed Boolean 属性允许您设置面板的初始状态,SuppressPostback 指明面板的展开是否应该完全是一个客户端操作。当 SuppressPostback 设置为 true 时,展开或折叠面板时不使用回发。这意味着无法对显示的数据进行更新。对于不会频繁改动的相对静止数据来说,这无疑是最佳的选择,因为它可以减少页面闪烁和网络流量。但是,如果需要在控件中显示变数较大的数据,则可以使用 UpdatePanel 控件,它也能最大程度减少闪烁。图 5 显示的是一个三级数据视图的最终用户界面。
Figure 5 Data View with Three Levels (单击该图像获得较大视图)
DataPager 和 ListView
ListView 控件通过新的 DataPager 控件可以提供分页功能。DataPager 是一种通用的分页控件,可用于任何实现 IPageableItemContainer 接口的数据绑定控件。在 ASP.NET 3.5 中,ListView 是唯一支持此接口的控件。
DataPager 控件可以显示内置的或基于模板的用户界面。无论何时用户通过单击跳转到新页面,DataPager 控件都会调用 IPageableItemContainer 接口的一个方法。此方法可以在分页控件中设置内部变量,以便在下次数据绑定操作过程中仅显示指定的数据页。
事实证明,选择正确的数据页仍是数据绑定控件(在本例中为 ListView)的一个难题。正如 ASP.NET 中的其他“视图”控件一样,ListView 控件依靠外部代码进行分页。如果数据通过数据源属性进行绑定,则用户代码应提供分页数据。如果并非如此,数据是通过数据源控件绑定的,则应该配置数据源控件属性以支持分页。
LinqDataSource 和 ObjectDataSource 控件都提供内置的分页功能。LinqDataSource 具有 AutoPage 属性,可用来启用或禁用默认的分页功能。对于分层数据,您还需要确保 LINQ 数据上下文包含正确的加载选项集。 LinqDataSource 的编程接口未提供对数据上下文对象设置 LoadOptions 属性的属性。但是,通过处理 ContextCreated 事件,您可以访问新创建的数据上下文并根据需要对其进行配置:
void LinqDataSource1_ContextCreated(
object sender, LinqDataSourceStatusEventArgs e)
{
// Get a reference to the data context
DataContext db = e.Result as DataContext;
if (db != null)
{
DataLoadOptions opt = new DataLoadOptions();
opt.LoadWith<Customer>(c => c.Orders);
opt.LoadWith<Order>(o => o.Employee);
opt.LoadWith<Order>(o => o.Order_Details);
db.LoadOptions = opt;
}
}
object sender, LinqDataSourceStatusEventArgs e)
{
// Get a reference to the data context
DataContext db = e.Result as DataContext;
if (db != null)
{
DataLoadOptions opt = new DataLoadOptions();
opt.LoadWith<Customer>(c => c.Orders);
opt.LoadWith<Order>(o => o.Employee);
opt.LoadWith<Order>(o => o.Order_Details);
db.LoadOptions = opt;
}
}
作为此操作的替代方法,您可以使用 ObjectDataSource 控件来提供数据并实现任何分页逻辑。然后,在业务对象中,可以使用 LINQ to SQL 或纯 ADO.NET 对数据进行访问。
我在使用 DataPager 和 ListView 时遇到的一个障碍很值得一提。我最初将包含 ListView 和 DataPager 的内容页面放在了同一个内容占位符中。然后我使用 PagedControlID 属性来引用 DataPager 中的 ListView 控件,如下所示。一切都能正常运行:
<asp:DataPager ID="DataPager1" runat="server"
PagedControlID="ListView1"
PageSize="5"
EnableViewState="false">
<Fields>
<asp:NextPreviousPagerField
ShowFirstPageButton="true"
ShowLastPageButton="true" />
</Fields>
</asp:DataPager>
PagedControlID="ListView1"
PageSize="5"
EnableViewState="false">
<Fields>
<asp:NextPreviousPagerField
ShowFirstPageButton="true"
ShowLastPageButton="true" />
</Fields>
</asp:DataPager>
接着,我将 DataPager 移动到同一个母版的另一个内容区域中。出乎意料的是,DataPager 竟然无法与 ListView 控件通信。出现这个问题的原因在于 DataPager 控件定位分页控件时所使用的算法。如果两个控件位于不同的命名容器中,则此算法将无法正常工作。为解决此问题,您需要使用分页控件的完整、唯一 ID(其中包括命名容器信息)来标识控件。遗憾的是,您无法通过声明的方式简单地设置此信息。
您不能使用 ASP 样式的代码块,因为如果使用它们来设置服务器控件的属性,它们将被视为文本串。您也不能使用数据绑定表达式 <%# ...%>,因为该表达式的计算时间过晚,无法满足 DataPager 的需要。Load 事件同样太迟,而且可能会导致 DataPager 出现异常。最简单的解决方案是在页面的 Init 事件中通过编程方式设置 PagedControlID 属性,如下所示:
protected void Page_Init(object sender, EventArgs e)
{
DataPager1.PagedControlID = ListView1.UniqueID;
}
{
DataPager1.PagedControlID = ListView1.UniqueID;
}
多项模板
与其他基于模板的控件和数据绑定的控件一样,ListView 为每个绑定数据项重复相同的项模板。那么如果想针对特定的项子集来更改它该怎样做呢?老实说,在我多年的 ASP.NET 编程经历中,还从未遇到过需要使用多个项模板的情况。有几次我曾经根据运行时条件自定义过 DataGrid 和 GridView 控件中一小组项的外观;但它必须要同时应用不同的样式属性组。
仅在极少数情况下,我曾通过编程方式在现有模板中添加了新控件(主要是 Label 控件或表格单元)。在触发数据绑定事件的数据绑定控件中,这项任务并不难实现——至少在您对所操作控件的内部结构有足够了解的情况下不难实现。
尽管在实际当中通过程序来注入控件是一种行之有效的解决方案,但我并不看好它。因此当有客户要求我修改网页中基于 ListView 的菜单时,我决定尝试使用不同的方法。在与图 1 类似的菜单中,我需要以水平方式而非垂直方式呈现一个子菜单的项。
ListView 控件通过在数据源中循环并应用下列算法来生成其标记。首先,它检查是否需要项分隔符。如果需要,它将实例化模板并创建数据项对象。数据项对象是项模板的容器,它包含有关视图中的项目索引和绑定数据源的信息。当项模板被实例化时,将触发 ItemCreated 事件。下一步是数据绑定。完成此步骤后,ItemDataBound 事件将被触发。
如您所见,没有可供处理的公共事件允许通过编程方式更改各个项的模板。您可以在 Init 或 Load 页面事件中更改模板,但这将影响所有绑定的项。如果处理 ItemCreated 事件并在其中设置 ItemTemplate 属性,则更改将影响下一项,而不会影响当前处理的项。您可能需要一个 ItemCreating 事件,但 ListView 控件并不会触发此类事件。解决方案只能是创建自己的 ListView 控件,如图 6 所示。
Figure 6 触发 ItemCreating 事件
namespace Samples.Controls
{
public class ListViewItemCreatingEventArgs : EventArgs
{
private int _dataItemIndex;
private int _displayIndex;
public ListViewItemCreatingEventArgs(int dataItemIndex,
int displayIndex) {
_dataItemIndex = dataItemIndex;
_displayIndex = displayIndex;
}
public int DisplayIndex {
get { return _displayIndex; }
set { _displayIndex = value; }
}
public int DataItemIndex {
get { return _dataItemIndex; }
set { _dataItemIndex = value; }
}
}
public class ListView : System.Web.UI.WebControls.ListView
{
public event EventHandler<ListViewItemCreatingEventArgs>
ItemCreating;
protected override ListViewDataItem CreateDataItem(int
dataItemIndex, int displayIndex) {
// Fire a NEW event: ItemCreating
if (ItemCreating != null)
ItemCreating(this, new ListViewItemCreatingEventArgs
(dataItemIndex, displayIndex));
// Call the base method
return base.CreateDataItem(_dataItemIndex, displayIndex);
}
}
}
{
public class ListViewItemCreatingEventArgs : EventArgs
{
private int _dataItemIndex;
private int _displayIndex;
public ListViewItemCreatingEventArgs(int dataItemIndex,
int displayIndex) {
_dataItemIndex = dataItemIndex;
_displayIndex = displayIndex;
}
public int DisplayIndex {
get { return _displayIndex; }
set { _displayIndex = value; }
}
public int DataItemIndex {
get { return _dataItemIndex; }
set { _dataItemIndex = value; }
}
}
public class ListView : System.Web.UI.WebControls.ListView
{
public event EventHandler<ListViewItemCreatingEventArgs>
ItemCreating;
protected override ListViewDataItem CreateDataItem(int
dataItemIndex, int displayIndex) {
// Fire a NEW event: ItemCreating
if (ItemCreating != null)
ItemCreating(this, new ListViewItemCreatingEventArgs
(dataItemIndex, displayIndex));
// Call the base method
return base.CreateDataItem(_dataItemIndex, displayIndex);
}
}
}
通过重载 CreateDataItem 方法,您可以在项目模板实例化的前一刻运行您的代码。CreateDataItem 方法在 ListView 类中声明为受保护的虚方法。如您在图 6 中所见,方法重载非常简单。首先触发自定义的 ItemCreating 事件,然后通过调用基类方法对其进行处理。
ItemCreating 事件将向用户代码传回一对整型值——数据源中项的绝对索引和特定页面的索引。例如,对于大小为 10 的页面,当 ListView 准备呈现第二页的第一项时,dataItemIndex 包含 11 个项,displayIndex 包含 1 个项。要使用新的 ItemCreating 事件,只需在自定义的 ListView 控件中声明方法和处理程序即可,如下面的代码所示:
<x:ListView runat="server" ID="ListView1"
ItemPlaceholderID="itemPlaceholder"
DataSourceID="ObjectDataSource1"
OnItemCreating="ListView1_ItemCreating">
<LayoutTemplate>
<div>
<asp:PlaceHolder runat="server" ID="itemPlaceholder" />
</div>
</LayoutTemplate>
</x:ListView>
ItemPlaceholderID="itemPlaceholder"
DataSourceID="ObjectDataSource1"
OnItemCreating="ListView1_ItemCreating">
<LayoutTemplate>
<div>
<asp:PlaceHolder runat="server" ID="itemPlaceholder" />
</div>
</LayoutTemplate>
</x:ListView>
在代码中,您可以采用如下方式来处理事件:
void ListView1_ItemCreating(
object sender, ListViewItemCreatingEventArgs e)
{
string url = "standard.ascx";
if (e.DisplayIndex % DataPager1.PageSize == 0)
url = "firstItem.ascx";
ListView1.ItemTemplate = Page.LoadTemplate(url);
}
object sender, ListViewItemCreatingEventArgs e)
{
string url = "standard.ascx";
if (e.DisplayIndex % DataPager1.PageSize == 0)
url = "firstItem.ascx";
ListView1.ItemTemplate = Page.LoadTemplate(url);
}
这里采用了两种不同的用户控件来呈现数据项。特定的用户控件由显示索引决定。除第一项外,所有项都共享同一个模板。图 7 显示了运行中的页面。
Figure 7 Multiple Item Templates
如果考虑到一般实际页面的复杂性,此解决方案可能显得有些过于简单。在大多数情况下,您需要根据显示内容使用不同的模板。您需要进一步增强自定义的 ListView 控件,以便在数据绑定过程中更改项模板。请看一下图 8 中的代码。
如果考虑到一般实际页面的复杂性,此解决方案可能显得有些过于简单。在大多数情况下,您需要根据显示内容使用不同的模板。您需要进一步增强自定义的 ListView 控件,以便在数据绑定过程中更改项模板。请看一下图 8 中的代码。
Figure 8 选择基于内容的模板
namespace Samples.Controls
{
public class ListViewItemCreatingEventArgs : EventArgs
{
private int _dataItemIndex;
private int _displayIndex;
public ListViewItemCreatingEventArgs(int dataItemIndex,
int displayIndex) {
_dataItemIndex = dataItemIndex;
_displayIndex = displayIndex;
}
public int DisplayIndex {
get { return _displayIndex; }
set { _displayIndex = value; }
}
public int DataItemIndex {
get { return _dataItemIndex; }
set { _dataItemIndex = value; }
}
}
public class ListView : System.Web.UI.WebControls.ListView
{
public event EventHandler<ListViewItemCreatingEventArgs>
ItemCreating;
private int _displayIndex;
private bool _shouldInstantiate = false;
protected override void InstantiateItemTemplate(Control container,
int displayIndex) {
if (_shouldInstantiate) {
base.InstantiateItemTemplate(container, displayIndex);
_shouldInstantiate = false;
}
}
protected override ListViewDataItem CreateDataItem(int
dataItemIndex, int displayIndex) {
// Fire a NEW event: ItemCreating
if (ItemCreating != null)
ItemCreating(this, new
ListViewItemCreatingEventArgs(dataItemIndex,
displayIndex));
// Cache for later
_displayIndex = displayIndex;
// Call the base method
return base.CreateDataItem(_dataItemIndex, displayIndex);
}
protected override void OnItemCreated(ListViewItemEventArgs e) {
base.OnItemCreated(e);
// You can proceed with template instantiation now
_shouldInstantiate = true;
InstantiateItemTemplate(e.Item, _displayIndex);
}
}
}
{
public class ListViewItemCreatingEventArgs : EventArgs
{
private int _dataItemIndex;
private int _displayIndex;
public ListViewItemCreatingEventArgs(int dataItemIndex,
int displayIndex) {
_dataItemIndex = dataItemIndex;
_displayIndex = displayIndex;
}
public int DisplayIndex {
get { return _displayIndex; }
set { _displayIndex = value; }
}
public int DataItemIndex {
get { return _dataItemIndex; }
set { _dataItemIndex = value; }
}
}
public class ListView : System.Web.UI.WebControls.ListView
{
public event EventHandler<ListViewItemCreatingEventArgs>
ItemCreating;
private int _displayIndex;
private bool _shouldInstantiate = false;
protected override void InstantiateItemTemplate(Control container,
int displayIndex) {
if (_shouldInstantiate) {
base.InstantiateItemTemplate(container, displayIndex);
_shouldInstantiate = false;
}
}
protected override ListViewDataItem CreateDataItem(int
dataItemIndex, int displayIndex) {
// Fire a NEW event: ItemCreating
if (ItemCreating != null)
ItemCreating(this, new
ListViewItemCreatingEventArgs(dataItemIndex,
displayIndex));
// Cache for later
_displayIndex = displayIndex;
// Call the base method
return base.CreateDataItem(_dataItemIndex, displayIndex);
}
protected override void OnItemCreated(ListViewItemEventArgs e) {
base.OnItemCreated(e);
// You can proceed with template instantiation now
_shouldInstantiate = true;
InstantiateItemTemplate(e.Item, _displayIndex);
}
}
}
CreateDataItem 方法将触发 ItemCreating 事件,并缓存显示索引以供以后使用。此外还重载了 InstantiateItemTemplate 方法以延迟实际的模板实例化。届时将使用私有的布尔标志来实现该目的。如前文所述,ListView 将在实例化项模板后启动数据绑定过程。
但是,在图 8 中的代码所示的实现中,在触发 ItemCreated 事件之前没有任何项模板真的被实例化。当引发 ItemCreated 事件时,数据项对象通过 DataItem 属性绑定到 ListView 项容器。通过在代码中处理 ItemCreated 事件,您可以根据绑定的数据项来决定要使用哪个项模板,如下所示:
protected override void OnItemCreated(ListViewItemEventArgs e)
{
base.OnItemCreated(e);
_shouldInstantiate = true;
InstantiateItemTemplate(e.Item, _displayIndex);
}
{
base.OnItemCreated(e);
_shouldInstantiate = true;
InstantiateItemTemplate(e.Item, _displayIndex);
}
在本例中,基类方法将触发页面的 ItemCreated 事件。随后,自定义的 ListView 控件将重置布尔标记,并调用方法实例化项模板。最后,项模板比在内置 ListView 控件中稍迟一些实例化,但是,在查看绑定数据项的内容后,您可以在 ItemCreated 事件处理程序中通过编程方法为每个项设置 ItemTemplate 属性(请参见图 9)。图 10 显示的是一个示例页面,其中蓝色模板用于男士,而粉色模板用于女士。
Figure 9 设置项模板
void ListView1_ItemCreated(object sender, ListViewItemEventArgs e)
{
// Grab a reference to the data item
ListViewDataItem currentItem = (e.Item as ListViewDataItem);
Employee emp = (Employee) currentItem.DataItem;
if (emp == null)
return;
// Apply your logic here
string titleOfCourtesy = emp.TitleOfCourtesy.ToLower();
string url = "forgentlemen.ascx";
if (titleOfCourtesy == "ms." || titleOfCourtesy == "mrs.")
url = "forladies.ascx";
// Set the item template to use
Samples.ListView ctl = (sender as Samples.Controls.ListView);
ctl.ItemTemplate = Page.LoadTemplate(url);
}
{
// Grab a reference to the data item
ListViewDataItem currentItem = (e.Item as ListViewDataItem);
Employee emp = (Employee) currentItem.DataItem;
if (emp == null)
return;
// Apply your logic here
string titleOfCourtesy = emp.TitleOfCourtesy.ToLower();
string url = "forgentlemen.ascx";
if (titleOfCourtesy == "ms." || titleOfCourtesy == "mrs.")
url = "forladies.ascx";
// Set the item template to use
Samples.ListView ctl = (sender as Samples.Controls.ListView);
ctl.ItemTemplate = Page.LoadTemplate(url);
}
Figure 10 A Standard Menu
总结
综上所述,新的 ASP.NET 3.5 ListView 控件是自从 ASP.NET 1.0 时代就已存在的 DataList 控件的改进版本。ListView 允许对生成的标记进行更严格的控制,并完全支持数据源对象。
在本专栏中,您学习了如何使用嵌套的 ListView 控件来构建可分页的多级数据视图,以及如何通过派生自定义的控件和重载一些方法来修改标准 ListView 控件的呈现过程。最终得到了一个支持多项模板的控件。没有任何其他 ASP.NET 数据绑定控件能提供如此程度的灵活性。
请将您想向 Dino 询问的问题和提出的意见发送至 cutting@microsoft.com.
Dino Esposito 是 IDesign 架构师和《Programming ASP.NET 3.5 Core Reference》的作者。Dino 定居于意大利,经常在世界各地的业内活动中发表演讲。您可以发送电子邮件至 cutting@microsoft.com 与他联系,或访问他在 weblogs.asp.net/despos 上开设的博客。