ListView提示和技巧

      概括来说,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 上开设的博客。

转载于:https://www.cnblogs.com/valens/archive/2008/06/05/1214498.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值