在上月刊中为您介绍了 ListView 控件,它是 ASP.NET 3.5 控件工具箱中的一个新成员。概括来说,ListView 是 DataList 控件的增强版本,它提供了对生成标记的更多控制,还支持分页功能,并与基于数据源的绑定模型实现了全面集成。

在本专栏中,我将深入介绍 ListView 模板和数据绑定的基础知识,以实现在实际页面中非常常见但却需要额外编码的一些功能。您将了解到如何使用嵌套的 ListView 控件来创建数据的分层视图,以及如何通过派生自定义 ListView 类来扩展 ListView 事件模型。

特别是,我将改进事件模型,以便您能够为不同的绑定数据项组使用不同的模板。例如,您可以对数据集中与给定标准匹配的所有数据项使用不同的模板。这绝不仅仅是简单地将特定项目设置为不同的样式;您可以在任何视图控件中仅通过处理 ItemDataBound 事件即可轻松完成此任务。

通常,菜单是由一系列使用 CSS 设计的 <li> 标记实现的。呈现平面菜单不会引起任何特殊的绑定问题,但如果需要一个或多个子菜单时会发生什么情况呢?在这种情况下,您可以使用内置的菜单控件,也可以借助 ListView 来创建极具个性化的呈现方式。顺便说一下,要注意在默认情况下,菜单控件使用基于表格的输出,这与 ListView 所提供的 CSS 友好输出截然不同。(要使菜单控件具有 CSS 友好输出,您需要安装并配置“CSS 控件适配器”工具包,它可以从 www.asp.net 下载。)

构建分层菜单

许多 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 源代码类似于下方所示:

  1. <h1>Something</h1>
  2. <ul>
  3. <li><a href="#">pellentesque</a></li>
  4. <li><a href="#">sociis natoque</a></li>
  5. <li><a href="#">semper</a></li>
  6. <li><a href="#">convallis</a></li>
  7. </ul>

如您所见,它包含一个顶级字符串,后跟一组链接列表。您可以使用第一个 ListView 创建 H1 元素,然后使用嵌套的 ListView(或类似的数据绑定控件)来呈现链接列表。在第一步中,您需要获取数据来填充菜单。理想情况下,您应该使用以下伪类型对象的集合来生成每个项目:

  1. class MenuItem {
  2. public string Title;
  3. public Collection<Link> Links;
  4. }
  5. class Link {
  6. public string Url;
  7. public string Text;
  8. }

填充 MenuItem 集合的合理方法是从 XML 文件中呈现信息。下面是该文档的一种可能架构:

  1. <Data>
  2. <RightMenuItems>
  3. <MenuItem>
  4. <Title>Something</Title>
  5. <Link url="..." text="pellentesque" />
  6. :
  7. </MenuItem>
  8. </RightMenuItems>
  9. </Data>

下面说明如何使用 LINQ to XML 来加载和处理内容:

  1. var doc = XDocument.Load(Server.MapPath("dataMap.xml"));
  2. var menu = (from e in doc.Descendants("RightMenuItems")
  3. select e).First();
  4. var menuLinks = from mi in menu.Descendants("MenuItem")
  5. select new
  6. {
  7. Title = mi.Value,
  8. Links = (...)
  9. };

加载文档之后,选择名为 RightMenuItems 的第一个节点,然后获取它的所有 MenuItem 子项。各个 MenuItem 节点的内容都被加载到一个新的匿名类型中,此类型包含两个属性——Title 和 Links。应该如何来填充 Links 集合呢?部分代码如下所示:

  1. Links = (from l in mi.Descendants("Link")
  2. select new {Url=l.Attribute("url").Value,
  3. Text=l.Attribute("text").Value})

下一步是将此复合数据绑定到用户界面。如前文所述,您要使用外部 ListView 来呈现标题,使用第二个嵌套的 ListView 来呈现子链接列表(请参见 图 2 )。请注意,最内侧的 ListView 必须使用 Eval 方法绑定到数据——任何其他方法都不会起作用:

Figure 2 嵌套的 ListView

  1. <asp:ListView runat="server" ID="RightMenuItems"
  2. ItemPlaceholderID="PlaceHolder2">
  3. <LayoutTemplate>
  4. <asp:PlaceHolder runat="server" ID="PlaceHolder2" />
  5. </LayoutTemplate>
  6. <ItemTemplate>
  7. <h1><%# Eval("Title") %></h1>
  8. <asp:ListView runat="server" ID="subMenu"
  9. ItemPlaceholderID="PlaceHolder3"
  10. DataSource='<%# Eval("Links") %>'>
  11. <LayoutTemplate>
  12. <ul>
  13. <asp:PlaceHolder runat="server" ID="PlaceHolder3" />
  14. </ul>
  15. </LayoutTemplate>
  16. <ItemTemplate>
  17. <li>
  18. <a href='<%# Eval("Url") %>'><%# Eval("Text") %></a>
  19. </li>
  20. </ItemTemplate>
  21. </asp:ListView>
  22. </ItemTemplate>
  23. </asp:ListView>
  1. <asp:ListView runat="server" ID="subMenu"
  2. ItemPlaceholderID="PlaceHolder3"
  3. DataSource='<%# Eval("Links") %>'>
  4. ...
  5. </asp:ListView>

通过将数据连接到顶级 ListView 来启动数据绑定过程。此时,ListView 的主体将完整地呈现出来,包括嵌套的 ListView。理论上说,您可以截取父 ListView 的 ItemDataBound 事件、遍历控件树、获取对子 ListView 的引用,然后以编程方式将其绑定到数据。如果这样做,将不会抛出异常,但内部 ListView 的绑定命令会丢失,因为它触发的时间太晚,无法影响呈现。另一方面,在任何数据绑定事件期间,都会在控件生命周期的合适时间自动计算一个数据绑定表达式。这样可以确保将正确的数据正确地绑定到用户界面。

创建分层视图

可以采用与填充分层菜单相同的模型来构建任何分层数据视图。在本例中,替代选项是使用 TreeView 控件来实现数据的多级表示。但是 TreeView 控件上的数据绑定要求使用分层数据源。在设计数据源结构和最终用户界面时,使用嵌套的 ListView 控件可以为您提供更大的灵活性。下面我们将详述这些概念。

假设您需要创建一个分层数据网格,并根据现有表关系在其中显示客户、订单和订单明细等信息。您应该如何检索数据并将其绑定到控件呢?请看一下 图 3 中的代码。您可以使用 LINQ to SQL 轻松地将数据加载到用于包含数据分层的对象模型中。请注意,在 LINQ to SQL 中运行查询时,您实际上仅检索那些显式请求的数据。换句话说,它仅提取图表的第一级,而不会同时自动加载任何相关的对象。

Figure 3 加载正确的数据

  1. Public Class DataCache
  2. {
  3. public IEnumerable GetCustomers()
  4. {
  5. NorthwindDataContext db = new NorthwindDataContext();
  6. DataLoadOptions opt = new DataLoadOptions();
  7. opt.LoadWith<Customer>(c => c.Orders);
  8. opt.LoadWith<Order>(o => o.Order_Details);
  9. db.LoadOptions = opt;
  10. var data = from c in db.Customers
  11. select new { c.CompanyName, c.Orders };
  12. return data.ToList();
  13. }
  14. public int GetCustomersCount()
  15. {
  16. // Return the number of customers
  17. NorthwindDataContext db = new NorthwindDataContext();
  18. return db.Customers.Count();
  19. }
  20. public IEnumerable GetCustomers(int maxRows, int startRowIndex)
  21. {
  22. if (maxRows < 0)
  23. return GetCustomers();
  24. NorthwindDataContext db = new NorthwindDataContext();
  25. DataLoadOptions opt = new DataLoadOptions();
  26. opt.LoadWith<Customer>(c => c.Orders);
  27. opt.LoadWith<Order>(o => o.Order_Details);
  28. db.LoadOptions = opt;
  29. var data = (from c in db.Customers
  30. select new {
  31. c.CompanyName,
  32. c.Orders
  33. }).Skip(startRowIndex).Take(maxRows);
  34. return data.ToList();
  35. }
  36. }
  37. NorthwindDataContext db = new NorthwindDataContext();
  38. DataLoadOptions opt = new DataLoadOptions();
  39. opt.LoadWith<Customer>(c => c.Orders);
  40. opt.LoadWith<Order>(o => o.Order_Details);
  41. db.LoadOptions = opt;

DataLoadOptions 类可用于修改 LINQ to SQL 引擎的默认行为,因此可以立即加载特定关系所引用的数据。 图 3 中的代码用来确保订单随客户一同加载,订单明细随订单一同加载。

LoadWith 方法根据指定关系加载数据。随后可使用 AssociateWith 方法筛选相关的预取对象,如下所示:

  1. opt.AssociateWith<Customer>(
  2. c => c.Orders.Where(o => o.OrderDate.Value.Year == 1997));

在本例中,提取客户数据时,只会预取于 1997 年发出的订单。当需要预取相关数据以及需要应用筛选器时,可以使用 AssociateWith 方法。您必须自行确保表之间不存在循环引用(例如,当您为某个客户加载订单然后又为某个订单加载客户时),如下所示:

  1. DataLoadOptions opt = new DataLoadOptions();
  2. opt.LoadWith<Customer> (c => c.Orders);
  3. opt.LoadWith<Order> (o => o.Customer);

现在已经准备好了所有数据,接下来就可以考虑进行绑定了。在本例中,一个二级 ListView 控件就能很好地完成此任务。将顶级 ListView 绑定到 Customer 对象集合,然后将最内侧的 ListView 绑定到各个已绑定 Customer 对象的 Orders 属性。 图 4 中的代码显示了用于三级分层视图的标记,在该视图中客户显示在第一级,并由最外层 ListView 的 ItemTemplate 属性来呈现。然后将嵌入的 ListView 绑定到订单。最后,嵌入 ListView 的 ItemTemplate 将包含一个 GridView,其中列出了每份订单的明细。

Figure 4 三级分层结构

  1. <asp:ListView ID="ListView1" runat="server"
  2. DataSourceID="ObjectDataSource1"
  3. ItemPlaceholderID="lvItemPlaceHolder">
  4. <LayoutTemplate>
  5. <asp:PlaceHolder runat="server" ID="lvItemPlaceHolder" />
  6. </LayoutTemplate>
  7. <ItemTemplate>
  8. <asp:Panel runat="server" ID="panelCustomerInfo"
  9. cssclass="customerInfo">
  10. <%# Eval("CompanyName") %>
  11. </asp:Panel>
  12. <asp:panel runat="server" ID="panelCustomerDetails"
  13. cssclass="customerDetails">
  14. <asp:ListView runat="server"
  15. DataSource='<%# Eval("Orders") %>'
  16. ItemPlaceholderID="lvOrdersItemPlaceHolder">
  17. <LayoutTemplate>
  18. <ul>
  19. <asp:PlaceHolder runat="server"
  20. ID="lvOrdersItemPlaceHolder" />
  21. </ul>
  22. </LayoutTemplate>
  23. <ItemTemplate>
  24. <li>
  25. Order #<%# Eval("OrderID") %>
  26. <span class="orderDate">
  27. placed on <%#
  28. ((DateTime)Eval("OrderDate")).ToString
  29. ("ddd, dd MMM yyyy") %>
  30. </span>
  31. <span class="orderEmployee">
  32. managed by <b><%# Eval("Employee.LastName") %></b>
  33. </span>
  34. <asp:GridView runat="server"
  35. DataSource='<%# Eval("Order_Details") %>'
  36. SkinID="OrderDetailsGridSkin" >
  37. </asp:GridView>
  38. </li>
  39. </ItemTemplate>
  40. </asp:ListView>
  41. </asp:panel>
  42. </ItemTemplate>
  43. </asp:ListView>

使用扩展器改进用户体验

坦白地讲,通过 图 4 中的代码得到的用户界面并不怎么吸引人。因为现在正在构建分层数据视图,所以使用展开/折叠面板对改进用户体验而言是最适合的解决方案。ASP.NET AJAX 控件工具包提供了一个现成的扩展器,当应用到面板服务器控件时,它可以为与各个客户和订单相关联的信息加入下拉列表效果。

使用 CollapsiblePanelExtender 控件在页面控件树中定义一个面板,此面板的展开和折叠将通过脚本来控制。不用说,作为页面开发人员,您不需要编写任何 JavaScript。展开和折叠面板所需的所有脚本都通过扩展器控件自动注入。让我们来看一下您可能希望在扩展器中设置的属性:

  1. <act:CollapsiblePanelExtender runat="server" ID="CollapsiblePanel1"
  2. TargetControlID="panelCustomerDetails"
  3. Collapsed="true"
  4. ScrollContents="true"
  5. SuppressPostback="true"
  6. ExpandedSize="250px"
  7. ImageControlID="Image1"
  8. ExpandedImage="~/images/collapse.jpg"
  9. CollapsedImage="~/images/expand.jpg"
  10. ExpandControlID="Image1"
  11. CollapseControlID="Image1">
  12. </act:CollapsiblePanelExtender>

需要对 图 4 中的代码进行少量改动,以支持可折叠的面板扩展器。特别地,您应该编辑名为 panelCustomerInfo 的面板,以添加用于展开和折叠子视图的按钮。下面是重新编写面板标记的一种方法:

  1. <asp:Panel ID="panelCustomerInfo" runat="server">
  2. <div class="customerInfo">
  3. <div style="float: left;"><%# Eval("CompanyName") %></div>
  4. <div style="float: right; vertical-align: middle;">
  5. <asp:ImageButton ID="Image1" runat="server"
  6. ImageUrl="~/images/expand.jpg"
  7. AlternateText="(Show Orders...)"/>
  8. </div>
  9. </div>
  10. </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 事件,您可以访问新创建的数据上下文并根据需要对其进行配置:

  1. void LinqDataSource1_ContextCreated(
  2. object sender, LinqDataSourceStatusEventArgs e)
  3. {
  4. // Get a reference to the data context
  5. DataContext db = e.Result as DataContext;
  6. if (db != null)
  7. {
  8. DataLoadOptions opt = new DataLoadOptions();
  9. opt.LoadWith<Customer>(c => c.Orders);
  10. opt.LoadWith<Order>(o => o.Employee);
  11. opt.LoadWith<Order>(o => o.Order_Details);
  12. db.LoadOptions = opt;
  13. }
  14. }

作为此操作的替代方法,您可以使用 ObjectDataSource 控件来提供数据并实现任何分页逻辑。然后,在业务对象中,可以使用 LINQ to SQL 或纯 ADO.NET 对数据进行访问。

我在使用 DataPager 和 ListView 时遇到的一个障碍很值得一提。我最初将包含 ListView 和 DataPager 的内容页面放在了同一个内容占位符中。然后我使用 PagedControlID 属性来引用 DataPager 中的 ListView 控件,如下所示。一切都能正常运行:

  1. <asp:DataPager ID="DataPager1" runat="server"
  2. PagedControlID="ListView1"
  3. PageSize="5"
  4. EnableViewState="false">
  5. <Fields>
  6. <asp:NextPreviousPagerField
  7. ShowFirstPageButton="true"
  8. ShowLastPageButton="true" />
  9. </Fields>
  10. </asp:DataPager>

接着,我将 DataPager 移动到同一个母版的另一个内容区域中。出乎意料的是,DataPager 竟然无法与 ListView 控件通信。出现这个问题的原因在于 DataPager 控件定位分页控件时所使用的算法。如果两个控件位于不同的命名容器中,则此算法将无法正常工作。为解决此问题,您需要使用分页控件的完整、唯一 ID(其中包括命名容器信息)来标识控件。遗憾的是,您无法通过声明的方式简单地设置此信息。

您不能使用 ASP 样式的代码块,因为如果使用它们来设置服务器控件的属性,它们将被视为文本串。您也不能使用数据绑定表达式 <%# ...%>,因为该表达式的计算时间过晚,无法满足 DataPager 的需要。Load 事件同样太迟,而且可能会导致 DataPager 出现异常。最简单的解决方案是在页面的 Init 事件中通过编程方式设置 PagedControlID 属性,如下所示:

  1. protected void Page_Init(object sender, EventArgs e)
  2. {
  3. DataPager1.PagedControlID = ListView1.UniqueID;
  4. }

与其他基于模板的控件和数据绑定的控件一样,ListView 为每个绑定数据项重复相同的项模板。那么如果想针对特定的项子集来更改它该怎样做呢?老实说,在我多年的 ASP.NET 编程经历中,还从未遇到过需要使用多个项模板的情况。有几次我曾经根据运行时条件自定义过 DataGrid 和 GridView 控件中一小组项的外观;但它必须要同时应用不同的样式属性组。

仅在极少数情况下,我曾通过编程方式在现有模板中添加了新控件(主要是 Label 控件或表格单元)。在触发数据绑定事件的数据绑定控件中,这项任务并不难实现——至少在您对所操作控件的内部结构有足够了解的情况下不难实现。

尽管在实际当中通过程序来注入控件是一种行之有效的解决方案,但我并不看好它。因此当有客户要求我修改网页中基于 ListView 的菜单时,我决定尝试使用不同的方法。在与 图 1 类似的菜单中,我需要以水平方式而非垂直方式呈现一个子菜单的项。

ListView 控件通过在数据源中循环并应用下列算法来生成其标记。首先,它检查是否需要项分隔符。如果需要,它将实例化模板并创建数据项对象。数据项对象是项模板的容器,它包含有关视图中的项目索引和绑定数据源的信息。当项模板被实例化时,将触发 ItemCreated 事件。下一步是数据绑定。完成此步骤后,ItemDataBound 事件将被触发。

如您所见,没有可供处理的公共事件允许通过编程方式更改各个项的模板。您可以在 Init 或 Load 页面事件中更改模板,但这将影响所有绑定的项。如果处理 ItemCreated 事件并在其中设置 ItemTemplate 属性,则更改将影响下一项,而不会影响当前处理的项。您可能需要一个 ItemCreating 事件,但 ListView 控件并不会触发此类事件。解决方案只能是创建自己的 ListView 控件,如 图 6 所示。

Figure 6 触发 ItemCreating 事件

  1. namespace Samples.Controls
  2. {
  3. public class ListViewItemCreatingEventArgs : EventArgs
  4. {
  5. private int _dataItemIndex;
  6. private int _displayIndex;
  7. public ListViewItemCreatingEventArgs(int dataItemIndex,
  8. int displayIndex) {
  9. _dataItemIndex = dataItemIndex;
  10. _displayIndex = displayIndex;
  11. }
  12. public int DisplayIndex {
  13. get { return _displayIndex; }
  14. set { _displayIndex = value; }
  15. }
  16. public int DataItemIndex {
  17. get { return _dataItemIndex; }
  18. set { _dataItemIndex = value; }
  19. }
  20. }
  21. public class ListView : System.Web.UI.WebControls.ListView
  22. {
  23. public event EventHandler<ListViewItemCreatingEventArgs>
  24. ItemCreating;
  25. protected override ListViewDataItem CreateDataItem(int
  26. dataItemIndex, int displayIndex) {
  27. // Fire a NEW event: ItemCreating
  28. if (ItemCreating != null)
  29. ItemCreating(this, new ListViewItemCreatingEventArgs
  30. (dataItemIndex, displayIndex));
  31. // Call the base method
  32. return base.CreateDataItem(_dataItemIndex, displayIndex);
  33. }
  34. }
  35. }

通过重载 CreateDataItem 方法,您可以在项目模板实例化的前一刻运行您的代码。CreateDataItem 方法在 ListView 类中声明为受保护的虚方法。如您在 图 6 中所见,方法重载非常简单。首先触发自定义的 ItemCreating 事件,然后通过调用基类方法对其进行处理。

ItemCreating 事件将向用户代码传回一对整型值——数据源中项的绝对索引和特定页面的索引。例如,对于大小为 10 的页面,当 ListView 准备呈现第二页的第一项时,dataItemIndex 包含 11 个项,displayIndex 包含 1 个项。要使用新的 ItemCreating 事件,只需在自定义的 ListView 控件中声明方法和处理程序即可,如下面的代码所示:

  1. <x:ListView runat="server" ID="ListView1"
  2. ItemPlaceholderID="itemPlaceholder"
  3. DataSourceID="ObjectDataSource1"
  4. OnItemCreating="ListView1_ItemCreating">
  5. <LayoutTemplate>
  6. <div>
  7. <asp:PlaceHolder runat="server" ID="itemPlaceholder" />
  8. </div>
  9. </LayoutTemplate>
  10. </x:ListView>

在代码中,您可以采用如下方式来处理事件:

  1. void ListView1_ItemCreating(
  2. object sender, ListViewItemCreatingEventArgs e)
  3. {
  4. string url = "standard.ascx";
  5. if (e.DisplayIndex % DataPager1.PageSize == 0)
  6. url = "firstItem.ascx";
  7. ListView1.ItemTemplate = Page.LoadTemplate(url);
  8. }

这里采用了两种不同的用户控件来呈现数据项。特定的用户控件由显示索引决定。除第一项外,所有项都共享同一个模板。 图 7 显示了运行中的页面。

Figure 7 Multiple Item Templates (单击该图像获得较大视图)

如果考虑到一般实际页面的复杂性,此解决方案可能显得有些过于简单。在大多数情况下,您需要根据显示内容使用不同的模板。您需要进一步增强自定义的 ListView 控件,以便在数据绑定过程中更改项模板。请看一下 图 8 中的代码。

Figure 8 选择基于内容的模板

  1. namespace Samples.Controls
  2. {
  3. public class ListViewItemCreatingEventArgs : EventArgs
  4. {
  5. private int _dataItemIndex;
  6. private int _displayIndex;
  7. public ListViewItemCreatingEventArgs(int dataItemIndex,
  8. int displayIndex) {
  9. _dataItemIndex = dataItemIndex;
  10. _displayIndex = displayIndex;
  11. }
  12. public int DisplayIndex {
  13. get { return _displayIndex; }
  14. set { _displayIndex = value; }
  15. }
  16. public int DataItemIndex {
  17. get { return _dataItemIndex; }
  18. set { _dataItemIndex = value; }
  19. }
  20. }
  21. public class ListView : System.Web.UI.WebControls.ListView
  22. {
  23. public event EventHandler<ListViewItemCreatingEventArgs>
  24. ItemCreating;
  25. private int _displayIndex;
  26. private bool _shouldInstantiate = false;
  27. protected override void InstantiateItemTemplate(Control container,
  28. int displayIndex) {
  29. if (_shouldInstantiate) {
  30. base.InstantiateItemTemplate(container, displayIndex);
  31. _shouldInstantiate = false;
  32. }
  33. }
  34. protected override ListViewDataItem CreateDataItem(int
  35. dataItemIndex, int displayIndex) {
  36. // Fire a NEW event: ItemCreating
  37. if (ItemCreating != null)
  38. ItemCreating(this, new
  39. ListViewItemCreatingEventArgs(dataItemIndex,
  40. displayIndex));
  41. // Cache for later
  42. _displayIndex = displayIndex;
  43. // Call the base method
  44. return base.CreateDataItem(_dataItemIndex, displayIndex);
  45. }
  46. protected override void OnItemCreated(ListViewItemEventArgs e) {
  47. base.OnItemCreated(e);
  48. // You can proceed with template instantiation now
  49. _shouldInstantiate = true;
  50. InstantiateItemTemplate(e.Item, _displayIndex);
  51. }
  52. }
  53. }

CreateDataItem 方法将触发 ItemCreating 事件,并缓存显示索引以供以后使用。此外还重载了 InstantiateItemTemplate 方法以延迟实际的模板实例化。届时将使用私有的布尔标志来实现该目的。如前文所述,ListView 将在实例化项模板后启动数据绑定过程。

但是,在 图 8 中的代码所示的实现中,在触发 ItemCreated 事件之前没有任何项模板真的被实例化。当引发 ItemCreated 事件时,数据项对象通过 DataItem 属性绑定到 ListView 项容器。通过在代码中处理 ItemCreated 事件,您可以根据绑定的数据项来决定要使用哪个项模板,如下所示:

  1. protected override void OnItemCreated(ListViewItemEventArgs e)
  2. {
  3. base.OnItemCreated(e);
  4. _shouldInstantiate = true;
  5. InstantiateItemTemplate(e.Item, _displayIndex);
  6. }

在本例中,基类方法将触发页面的 ItemCreated 事件。随后,自定义的 ListView 控件将重置布尔标记,并调用方法实例化项模板。最后,项模板比在内置 ListView 控件中稍迟一些实例化,但是,在查看绑定数据项的内容后,您可以在 ItemCreated 事件处理程序中通过编程方法为每个项设置 ItemTemplate 属性(请参见 图 9 )。 图 10 显示的是一个示例页面,其中蓝色模板用于男士,而粉色模板用于女士。

Figure 9 设置项模板

  1. void ListView1_ItemCreated(object sender, ListViewItemEventArgs e)
  2. {
  3. // Grab a reference to the data item
  4. ListViewDataItem currentItem = (e.Item as ListViewDataItem);
  5. Employee emp = (Employee) currentItem.DataItem;
  6. if (emp == null)
  7. return;
  8. // Apply your logic here
  9. string titleOfCourtesy = emp.TitleOfCourtesy.ToLower();
  10. string url = "forgentlemen.ascx";
  11. if (titleOfCourtesy == "ms." || titleOfCourtesy == "mrs.")
  12. url = "forladies.ascx";
  13. // Set the item template to use
  14. Samples.ListView ctl = (sender as Samples.Controls.ListView);
  15. ctl.ItemTemplate = Page.LoadTemplate(url);
  16. }

Figure 10 A Standard Menu (单击该图像获得较大视图)

综上所述,新的 ASP.NET 3.5 ListView 控件是自从 ASP.NET 1.0 时代就已存在的 DataList 控件的改进版本。ListView 允许对生成的标记进行更严格的控制,并完全支持数据源对象。

在本专栏中,您学习了如何使用嵌套的 ListView 控件来构建可分页的多级数据视图,以及如何通过派生自定义的控件和重载一些方法来修改标准 ListView 控件的呈现过程。最终得到了一个支持多项模板的控件。没有任何其他 ASP.NET 数据绑定控件能提供如此程度的灵活性。