代码改变世界

ListView提示和技巧

2008-06-05 15:20  Valens  阅读(1287)  评论(0编辑  收藏  举报
      概括来说,ListView 是 DataList 控件的增强版本,它提供了对生成标记的更多控制,还支持分页功能,并与基于数据源的绑定模型实现了全面集成。
      在本专栏中,我将深入介绍 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><href="#">pellentesque</a></li>
    
<li><href="#">sociis natoque</a></li>
    
<li><href="#">semper</a></li>
    
<li><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;
}
      填充 MenuItem 集合的合理方法是从 XML 文件中呈现信息。下面是该文档的一种可能架构:
<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 
= ()
                };

      加载文档之后,选择名为 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})

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

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

创建分层视图
      可以采用与填充分层菜单相同的模型来构建任何分层数据视图。在本例中,替代选项是使用 TreeView 控件来实现数据的多级表示。但是 TreeView 控件上的数据绑定要求使用分层数据源。在设计数据源结构和最终用户界面时,使用嵌套的 ListView 控件可以为您提供更大的灵活性。下面我们将详述这些概念。
      假设您需要创建一个分层数据网格,并根据现有表关系在其中显示客户、订单和订单明细等信息。您应该如何检索数据并将其绑定到控件呢?请看一下图 3 中的代码。您可以使用 LINQ to SQL 轻松地将数据加载到用于包含数据分层的对象模型中。请注意,在 LINQ to SQL 中运行查询时,您实际上仅检索那些显式请求的数据。换句话说,它仅提取图表的第一级,而不会同时自动加载任何相关的对象。
      DataLoadOptions 类可用于修改 LINQ to SQL 引擎的默认行为,因此可以立即加载特定关系所引用的数据。图 3 中的代码用来确保订单随客户一同加载,订单明细随订单一同加载。
      LoadWith 方法根据指定关系加载数据。随后可使用 AssociateWith 方法筛选相关的预取对象,如下所示:
opt.AssociateWith<Customer>(
     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); 

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

使用扩展器改进用户体验
      坦白地讲,通过图 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>

      需要对图 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> 

      该按钮与客户名称位于同一行,使用右对齐图像来呈现。扩展器的 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;
    }
}

      作为此操作的替代方法,您可以使用 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>

      接着,我将 DataPager 移动到同一个母版的另一个内容区域中。出乎意料的是,DataPager 竟然无法与 ListView 控件通信。出现这个问题的原因在于 DataPager 控件定位分页控件时所使用的算法。如果两个控件位于不同的命名容器中,则此算法将无法正常工作。为解决此问题,您需要使用分页控件的完整、唯一 ID(其中包括命名容器信息)来标识控件。遗憾的是,您无法通过声明的方式简单地设置此信息。
      您不能使用 ASP 样式的代码块,因为如果使用它们来设置服务器控件的属性,它们将被视为文本串。您也不能使用数据绑定表达式 <%# ...%>,因为该表达式的计算时间过晚,无法满足 DataPager 的需要。Load 事件同样太迟,而且可能会导致 DataPager 出现异常。最简单的解决方案是在页面的 Init 事件中通过编程方式设置 PagedControlID 属性,如下所示:
protected void Page_Init(object sender, EventArgs e)
{
   DataPager1.PagedControlID 
= ListView1.UniqueID;
}

多项模板
      与其他基于模板的控件和数据绑定的控件一样,ListView 为每个绑定数据项重复相同的项模板。那么如果想针对特定的项子集来更改它该怎样做呢?老实说,在我多年的 ASP.NET 编程经历中,还从未遇到过需要使用多个项模板的情况。有几次我曾经根据运行时条件自定义过 DataGrid 和 GridView 控件中一小组项的外观;但它必须要同时应用不同的样式属性组。
      仅在极少数情况下,我曾通过编程方式在现有模板中添加了新控件(主要是 Label 控件或表格单元)。在触发数据绑定事件的数据绑定控件中,这项任务并不难实现——至少在您对所操作控件的内部结构有足够了解的情况下不难实现。
      尽管在实际当中通过程序来注入控件是一种行之有效的解决方案,但我并不看好它。因此当有客户要求我修改网页中基于 ListView 的菜单时,我决定尝试使用不同的方法。在与图 1 类似的菜单中,我需要以水平方式而非垂直方式呈现一个子菜单的项。
      ListView 控件通过在数据源中循环并应用下列算法来生成其标记。首先,它检查是否需要项分隔符。如果需要,它将实例化模板并创建数据项对象。数据项对象是项模板的容器,它包含有关视图中的项目索引和绑定数据源的信息。当项模板被实例化时,将触发 ItemCreated 事件。下一步是数据绑定。完成此步骤后,ItemDataBound 事件将被触发。
      如您所见,没有可供处理的公共事件允许通过编程方式更改各个项的模板。您可以在 Init 或 Load 页面事件中更改模板,但这将影响所有绑定的项。如果处理 ItemCreated 事件并在其中设置 ItemTemplate 属性,则更改将影响下一项,而不会影响当前处理的项。您可能需要一个 ItemCreating 事件,但 ListView 控件并不会触发此类事件。解决方案只能是创建自己的 ListView 控件,如图 6 所示。
      通过重载 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>

      在代码中,您可以采用如下方式来处理事件:
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);
}

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

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

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