Cutting Edge

ListView Tips and Tricks

Dino Esposito

Code download available at:CuttingEdge2008_04.exe(477 KB)

Contents

Building a Hierarchical Menu
Building a Hierarchical View
Improve the User Experience with Extenders
DataPager and ListView
Multiple Item Templates
Summing It Up

In last month's issue, you were introduced to the ListView control, a new addition to the ASP.NET 3.5 control toolbox. To recap, the ListView is an enhanced version of the DataList control that provides more control over generated markup, support for paging, and full integration with the data source-based binding model.

In this column, I'll move beyond the basics of ListView templates and data binding to implement features that are fairly common in real-world pages but require some extra coding. You'll see how to use nested ListView controls to create hierarchical views of data and how to extend the eventing model of the ListView by deriving a custom ListView class.

In particular, I'll refine the eventing model so you can use a different template for different groups of bound data items. For example, you'll be able to use a different template for all data items in a data set that match some given criteria. This will go far beyond simply styling certain items differently; you can do that easily in any view control by simply handling the ItemDataBound event.

Usually menus are implemented as a sequence of <li> tags that are styled using CSS. Rendering a flat menu doesn't pose any particular binding problems, but what happens when you need one or more sub-menus? In this case, you can either use the built-in Menu control or create a more custom rendering strategy by using a ListView. Incidentally, also note that by default the Menu control uses a table-based output unlike the more CSS-friendly output that you can obtain with a ListView. (To obtain a CSS-friendly output for a Menu control, you need to install and configure the CSS Control Adapter Toolkit, which you can download from www.asp.net.)

Building a Hierarchical Menu

Many Web apps offer a vertical menu on the left or right side of the page. This menu enables the user to navigate to pages two or more nesting levels deep. The ASP.NET Menu control is definitely a viable option here. However, I tend to use the Menu control only when I have a hierarchical data source (typically, an XML file) to feed the menu and when I need to create fly-out sub-menus.

For a static multi-level list of items, I opt for a repeater-like control to output markup created by a UI design team. In ASP.NET 3.5, the repeater-like control of choice is the ListView control.

Consider a menu like the one in Figure 1. It's shown in the CoffeeNCream free HTML template you can download from oswd.org. In the sample page, I simply incorporated the HTML markup in an ASP.NET Master Page.

Figure 1 A Standard Menu

Figure 1** A Standard Menu **(Click the image for a larger view)

The HTML source code of an item of the right-side menu looks like the following:

<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>

As you can see, it contains a top-level string followed by a list of links. You use a first ListView to create the H1 elements and then a nested ListView (or a similar data-bound control) to render the list of links. As a first step, you need to grab data to populate the menu. Ideally, you would use a collection of the following pseudotype objects to generate each item:

class MenuItem {
  public string Title;
  public Collection<Link> Links;
}

class Link  {
  public string Url;
  public string Text;
}

A reasonable method for populating a MenuItem collection is to render information from an XML file. Here's a possible schema for the document:

<Data>
  <RightMenuItems>
     <MenuItem>
       <Title>Something</Title>
       <Link url="..." text="pellentesque" />
         :
     </MenuItem>
  </RightMenuItems>
</Data>

The following demonstrates how to use LINQ to XML to load and process the content:

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 = (...)
                };

After loading the document, you select the first node named RightMenuItems and then grab all of its MenuItem children. The content of each MenuItem node is loaded into a new anonymous type with two properties—Title and Links. How would you populate the Links collection? Here's some code:

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

The next step is to bind this compound data to the user interface. As mentioned, you use an outer ListView to render the title and a second, nested ListView to render the list of child links (see Figure 2). Note that the innermost ListView must be bound to data using the Eval method—any other approach will not work:

Figure 2 Nested ListViews

<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>

<asp:ListView runat="server" ID="subMenu" 
    ItemPlaceholderID="PlaceHolder3"
    DataSource='<%# Eval("Links") %>'>
    ...
</asp:ListView>

You start the data binding process by attaching data to the top-level ListView. When this happens, the body of the ListView is rendered entirely, including the nested ListView. You could, in theory, intercept the ItemDataBound event of the parent ListView, walk your way through the control tree, grab a reference to the child ListView, and bind it programmatically to data. If you do, you won't throw an exception, but the binding command on the inner ListView is lost because it fires too late to affect the rendering. A data-binding expression, on the other hand, is automatically evaluated during any data-binding event at just the right point in the control lifecycle. This ensures that the correct data is bound properly to the user interface.

Building a Hierarchical View

The same model for populating a hierarchical menu can be employed for building any hierarchical view of data. In this case, an alternate option would be to use a TreeView control to prepare a multi-level representation of the data. Data binding on a TreeView control, though, requires a hierarchical data source. Using nested ListView controls gives you more flexibility with regard to both the structure of the data source and the resulting user interface. Let's expand on these concepts.

Suppose you need to create a hierarchical grid of data where customers, orders, and order details are displayed according to existing table relationships. How would you retrieve the data and bind it to the control? Have a look at the code in Figure 3. You can use LINQ to SQL to easily load data into an object model that naturally lends itself to containing a hierarchy of data. Note that when you run a query in LINQ to SQL, you actually retrieve only the data you requested explicitly. Put another way, only the first level of the graph is fetched; any related objects are not automatically loaded at the same time.

Figure 3 Loading the Proper Data

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;

The DataLoadOptions class can be used to modify the default behavior of the LINQ to SQL engine so that data referenced by a specific relationship is loaded immediately. The code in Figure 3 ensures that orders are loaded along with customers and that details are loaded along with orders.

The LoadWith method loads data according to the specified relation. The AssociateWith method can then allow for filtering related prefetched objects, as shown here:

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

In this example, only orders issued in the year 1997 are prefetched when customer data is fetched. You use the AssociateWith method when you need to prefetch related data and also when you need to apply a filter. It is entirely up to you to ensure that no cyclical references exist between tables—for instance, when you load orders for a customer and then load the customer for an order, as shown here:

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

Now that you have all the data ready, you can start thinking about binding. In this case, a two-level ListView control will do the trick quite well. You bind the top-level ListView to the collection of Customer objects and the innermost ListView to the Orders property of each bound Customer object. The code in Figure 4 shows the markup for a three-level hierarchical view where customers are displayed on the first level and are rendered by the ItemTemplate property of the outermost ListView. The embedded ListView is then bound to orders. And finally, the ItemTemplate of the embedded ListView contains a GridView to list the details of each order.

Figure 4 Three-Level Hierarchy

<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>

Improve the User Experience with Extenders

Frankly, the user interface you get from the code in Figure 4 is not very appealing. Because a hierarchical data view is being constructed, an expand/collapse panel would be a particularly apt solution for improving the user experience. The ASP.NET AJAX Control Toolkit provides a ready-made extender that, when applied to a Panel server control, can add a dropdown effect to the information associated with each customer and order.

Use the CollapsiblePanelExtender control to define a panel in the page control tree that will be expanded and collapsed via script. Needless to say, as a page developer you will not have to write any JavaScript. All of the script required for expanding and collapsing the panel is silently injected by the extender control. Let's have a look at the properties you might want to set on the extender:

<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>

Some slight changes to the code in Figure 4 are needed to support the collapsible panel extender. In particular, you should edit the panel named panelCustomerInfo to add the button used to expand and collapse the child view. Here's one way to rewrite the panel's markup:

<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>

The button is rendered using a right-justified image on the same row as the customer name. The TargetControlID property on the extender references the panel in the page that will be collapsed and expanded. This is the panel that physically contains orders and order details. As you see in Figure 4, it is the panel named panelCustomerDetails.

The ExpandControlID and CollapseControlID attributes indicate the ID of the elements that, when clicked, expand and collapse the target panel. If you plan to use different images to reflect the state of the panel, then you need to also specify the ID of an image control. This information belongs to the ImageControlID attribute. The ImageControlID is associated with two other properties, CollapsedImage and ExpandedImage—these hold the URL of the images.

The ExpandedSize property sets the maximum height in pixels allowed for the expanded panel. By default, any content that exceeds the height is just clipped out. However, if you set the ScrollContents property to true, a vertical scrollbar is added to let users scroll the entire contents.

Finally, the Collapsed Boolean property allows for you to set the initial state of the panel and SuppressPostback indicates whether the expansion of the panel should be a completely client-side operation. When SuppressPostback is true, no postback is used to expand or collapse the panel. This means that no updates are possible on the displayed data. For relatively static data that doesn't change frequently, this is definitely the best possible choice as it reduces page flickering and network traffic. However, if you need to display data more dynamically in the control, you can still minimize flickering by using an UpdatePanel control. Figure 5 shows the resulting user interface of a tri-level data view.

Figure 5 Data View with Three Levels

Figure 5** Data View with Three Levels **(Click the image for a larger view)

DataPager and ListView

The ListView control offers paging facilities through the new DataPager control. The DataPager is a general-purpose paging control that can be used by any data-bound control that implements the IPageableItemContainer interface. As of ASP.NET 3.5, the ListView is the only control that supports this interface.

The DataPager control can display a built-in or template-based user interface. Whenever the user clicks to jump to a new page, the DataPager control invokes a method on the IPageableItemContainer interface. This method is expected to set internal variables in the paged control so that only a specific page of data is displayed during the next data-binding operation.

It turns out that selecting the right page of data remains the problem of the data-bound control—in this case, the ListView. Just as with other "view" controls in ASP.NET, the ListView control relies on external code for paging. If data is bound through the data source property, then user code should provide for the paged data. Otherwise, if data is bound through a data source control, you should configure the data source control properly to support paging.

Both the LinqDataSource and ObjectDataSource controls offer built-in paging capabilities. The LinqDataSource has the AutoPage property for enabling or disabling default paging. For hierarchical data, you also need to ensure that the LINQ data context has proper load options set. The programming interface of the LinqDataSource doesn't have properties for setting the LoadOptions property on the data context object. However, by handling the ContextCreated event, you can access the newly created data context and configure it as you like:

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;
    }
}

As an alternative to this action, you can use the ObjectDataSource control for providing data and implementing any paging logic. Then, in the business object you can either use LINQ to SQL or plain ADO.NET for data access.

A snag I ran into while using DataPager and ListView together is worth mentioning, though. I initially had a content page with ListView and DataPager hosted in the same content placeholder. I referenced the ListView control in the DataPager using the PagedControlID property, as shown in the following. It worked just fine:

<asp:DataPager ID="DataPager1" runat="server" 
     PagedControlID="ListView1"
     PageSize="5" 
     EnableViewState="false">
  <Fields>
     <asp:NextPreviousPagerField 
        ShowFirstPageButton="true" 
        ShowLastPageButton="true" />
  </Fields>
</asp:DataPager>

Next, I moved the DataPager to another content area in the same master. And all of a sudden, the DataPager was unable to communicate with the ListView control. The problem lies with the algorithm used by the DataPager control to locate the paged control. This algorithm doesn't work if the two controls are hosted by different naming containers. To work around the issue, you need to identify the paged control using its full, unique ID (that includes naming container information). Unfortunately, you can't easily set this information declaratively.

You can't use ASP-style code blocks because they are treated like literals if used to set a property of a server control. You can't use a data binding expression <%# ... %> either because the expression is evaluated too late for the needs of the DataPager. The Load event is too late and would cause the DataPager to fire an exception. The simplest workaround is to set the PagedControlID property programmatically in the page's Init event, like so:

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

Multiple Item Templates

Just like other template-based and data-bound controls, the ListView repeats the same item template for each bound data item. So what if you want to change it for a certain subset of items? Honestly, I have to admit that I never had the need to use more than one item template in years of ASP.NET programming. Several times I customized the appearance of a small group of items in DataGrid and GridView controls based on runtime conditions; however, that always entailed applying a different set of style attributes.

Only in very few cases did I programmatically add new controls (mostly Label controls or table cells) to the existing template. In data-bound controls that fire data binding events, this is not a really difficult task—at least not if you have intimate knowledge of the internal structure of the controls you are manipulating.

Although programmatic injection of controls is a solution that worked really well in practice, it never captured my heart. So I decided to try a different route when a customer asked me to modify a ListView-based menu in a Web page. In a menu similar to the one in Figure 1, I needed to render the items of one submenu horizontally rather than vertically.

The ListView control generates its markup by looping through the data source and applying the following algorithm. First, it checks whether an item separator is required. If so, it instantiates the template and creates the data item object. The data item object is the container of the item template and carries information about the index of the item in the view and the bound data source. When the item template is instantiated, the ItemCreated event fires. The next step is data binding. After this has completed, the ItemDataBound event is fired.

As you can see, there's no public event that you can handle that allows programmatically changing the template for each item. You can change the template in the Init or Load page event, but that would be for all bound items. If you handle ItemCreated and set the ItemTemplate property there, the change will affect the next item, but not the item being currently processed. You would need an ItemCreating event, but no such an event is fired by the ListView control. The solution, then, is to create your own ListView control, as in Figure 6.

Figure 6 Firing an ItemCreating Event

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);
    }
  }
}

By overriding the CreateDataItem method, you have a chance to run your code just before the item template is instantiated. The CreateDataItem method is declared protected and virtual in the ListView class. As you can see in Figure 6, the method override is quite simple. You first fire a custom ItemCreating event and then proceed by calling the base method.

The ItemCreating event passes a couple of integers back to the user code—the absolute index of the item in the data source and the page-specific index. For example, for a page size of 10, when the ListView is working on rendering the first item of the second page, dataItemIndex contains 11 items and displayIndex contains 1 item. In order to use the new ItemCreating event, simply declare the method and handler on your custom ListView control, as you can see in the following code:

<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>

In your code, you can handle the event like so:

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);
}

Here, two different user controls are employed to render the data items. The specific user control is determined by the display index. All items share the same template except the first. Figure 7 shows the page in action.

Figure 7 Multiple Item Templates

Figure 7** Multiple Item Templates **(Click the image for a larger view)

When you think about the complexity of common real-world pages, this solution appears too simple. More often than not, you need to use different templates based on the content to display. You need to further enhance the custom ListView control to change the item template within the data-binding process. Have a look at the code in Figure 8.

Figure 8 Choosing Template Based on Content

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);
    }
  }
}

The CreateDataItem method fires the ItemCreating event and caches the display index for later use. In addition, the InstantiateItemTemplate method is overridden to delay the actual template instantiation. A private boolean flag is used for that purpose. As mentioned, the ListView starts the data-binding process after instantiating the item template.

In the implementation shown in the code in Figure 8, however, no item template is really instantiated until the ItemCreated event is fired. When the ItemCreated event is raised, the data item object is bound to the ListView item container through the DataItem property. By handling the ItemCreated event in your code, you can decide which item template to use based on the bound data item, as you can see here:

protected override void OnItemCreated(ListViewItemEventArgs e)
{
   base.OnItemCreated(e);

   _shouldInstantiate = true;
   InstantiateItemTemplate(e.Item, _displayIndex);
}

In this case, the base method fires the ItemCreated event to the page. After that, the custom ListView control resets the Boolean flag and invokes the method to instantiate the item template. In the end, the item template is instantiated a bit later than in the built-in ListView control, but you can set the ItemTemplate property for each item programmatically in the ItemCreated event handler after looking at the content of the bound data item (see Figure 9). Figure 10 shows a sample page where a blue template is used for men and a pink template is used for women.

Figure 9 Setting the Item Template

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);
}

Figure 10 A Standard Menu

Figure 10** A Standard Menu **(Click the image for a larger view)

Summing It Up

At the end of the day, the new ASP.NET 3.5 ListView control is a revamped version of the DataList control that has existed since ASP.NET 1.0. The ListView allows for tighter control over the generated markup and fully supports data source objects.

In this column, you saw how to use nested ListView controls to build pageable and multi-level data views and how to modify the standard ListView's rendering process by deriving a custom control and overriding a few methods. The final result is a control that supports multiple item templates. No other ASP.NET data-bound control provides such a level of flexibility.

Send your questions and comments for Dino to cutting@microsoft.com.

Dino Esposito is an IDesign architect and the author of Programming ASP.NET 3.5 Core Reference. Based in Italy, Dino is a frequent speaker at industry events worldwide. You can reach him at cutting@microsoft.com or join his blog at weblogs.asp.net/despos.