My musings about .NET and what not

Nested ListViews and More - Working with Databound Controls Inside the ListView

To take full advantage of all the great stuff the ListView has to offer, it's important to understand how to properly bind controls that are nested inside of it. Once you know how, it's easy to embed any data-bound control like DropDownLists, CheckBoxLists, and even other ListViews inside your ListView. Here, I'm going to demonstrate two different ways to do this.

As we all know, the ListView is the new, super-duper data bound control that Microsoft introduced with ASP.NET 3.5. You could think of the ListView as what you might get if a GridView and a Repeater had a baby. Like the GridView, the ListView can display, select, edit, delete, page, and sort records. And like the Repeater, the ListView is entirely template-driven. Also, the ListView supports inserting new records, functionality provided by neither the GridView nor the Repeater.

The ListView is just so fun-n-flexible, I call it the Gumby of Data Bound Controls. Once it finds its way into your toybox, you'll want to play with it every chance you get.

The Problem

Let's pretend you're building a blog-like application. You would probably want to have a page that shows a list of your blog articles. That could be a ListView. And under each article, you'd want to have a list of tags for that article. That list of tags could also be a ListView. The tags ListView would be nested inside the articles ListView.

You might end up with something like the following:

image

Figure 1

How would you approach this? Well, binding the outer ListView (the articles) is not very tricky at all. You could use an ObjectDataSource, an SqlDataSource, a  LinqDataSource -- in fact, any data source control you want. Configure the data source control, specify its DataSourceID in the ListView, throw down a few databinding expressions, and bada bing! -- you're halfway home.

<%@ Page Language="C#" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
   <title>Articles</title>
</head>
<body>
   <form id="form1" runat="server">
      <div>         
         <asp:ListView ID="lvArticles" runat="server" DataSourceID="objArticles"
            ItemPlaceholderID="itemPlaceHolder1">
            <ItemTemplate>
               <p>
                  <asp:Label ID="lblArticleTitle" runat="server" Font-Bold="true"
                     Text='<%# Eval("Title") %>' />
                  <br />
                  <asp:Label ID="lblPublishedDate" runat="server" Font-Italic="true"
                     Text='<%# Eval("PublishedDate","{0:MMM d, yyyy}") %>' />
                  <br />
                  Tags:                  
                  <asp:ListView ID="lvTags" runat="server" ItemPlaceholderID="itemPlaceHolder2" >
                     <ItemTemplate>
                        <asp:Label ID="lblTagName" runat="server" Text='<%# Eval("TagName") %>' />
                     </ItemTemplate>
                     <LayoutTemplate>
                        <asp:PlaceHolder ID="itemPlaceHolder2" runat="server" />
                     </LayoutTemplate>
                  </asp:ListView>
               </p>
            </ItemTemplate>
            <LayoutTemplate>
               <asp:PlaceHolder ID="itemPlaceHolder1" runat="server" />
            </LayoutTemplate>
         </asp:ListView>
         <asp:ObjectDataSource ID="objArticles" runat="server" SelectMethod="GetArticles"
            TypeName="LD.Blog.Article" />
      </div>
   </form>
</body>
</html>

Figure 2

In Figure 2, we're using an ObjectDataSource (objArticles) to bind the outer ListView (lvArticles). Of course, this assumes I've set up a class called LD.Blog.Article, that contains a method called GetArticles that returns article data.

using System;
using System.Collections.Generic;
 
namespace LD.Blog
{
   public class Article
   {
      public int ArticleID { get; set; }
      public string Title { get; set; }
      public DateTime PublishedDate { get; set; }
      public static IEnumerable<Article> GetArticles()
      {
         // data access code not shown
      }
   }
}

Figure 3

I've purposely left out the specific data access code from Figure 3. It's not important exactly how we're fetching the data in the GetArticles method. This could be LINQ to SQL, classic ADO.NET, or whatever. All that matters is that we're returning a collection of Article objects that the lvArticles ListView can bind to.

Notice also from Figure 2 that we've nested a second ListView (lvTags) inside the <ItemTemplate> of lvArticles. This what we have so far:

image

Figure 4

Of course, there are no tags showing. That's because we haven't bound the inner lvTags ListView to anything yet. Let's see what we have to do to accomplish that.

Solution 1: Using a Second DataSource Control

Let's further assume you also have a class called LD.Blog.Tag that contains a method called GetTagsByArticle. This method takes as an argument the ID of an article, and returns the Tags belonging to that Article.

using System.Collections.Generic;
 
namespace LD.Blog
{
   public class Tag
   {
      public int TagID { get; set; }
      public string TagName { get; set; }
      public static IEnumerable<Tag> GetTagsByArticle(int articleID)
      {
         // data access code not shown
      }
   }
}

Figure 5

Given this, it would be natural to assume we could just embed a second ObjectDataSource into lvArticles and bind lvTags to that -- and of course, we can.

<%@ Page Language="C#" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
   <title>Articles - Nested ListView, ObjectDataSource</title>
</head>
<body>
   <form id="form1" runat="server">
      <div>
         <asp:ListView ID="lvArticles" runat="server" DataSourceID="objArticles"
            ItemPlaceholderID="itemPlaceHolder1" OnItemDataBound="lvArticles_ItemDataBound">
            <ItemTemplate>
               <p>
                  <asp:Label ID="lblArticleTitle" runat="server" Font-Bold="true"
                     Text='<%# Eval("Title") %>' />
                  <br />
                  <asp:Label ID="lblPublishedDate" runat="server" Font-Italic="true"
                     Text='<%# Eval("PublishedDate","{0:MMM d, yyyy}") %>' />
                  <br />
                  Tags:
                  <asp:ListView ID="lvTags" runat="server" ItemPlaceholderID="itemPlaceHolder2"
                     DataSourceID="objTags">
                     <ItemTemplate>
                        <asp:Label ID="lblTagName" runat="server" Text='<%# Eval("TagName") %>' />
                     </ItemTemplate>
                     <LayoutTemplate>
                        <asp:PlaceHolder ID="itemPlaceHolder2" runat="server" />
                     </LayoutTemplate>
                  </asp:ListView>
                  <asp:ObjectDataSource ID="objTags" runat="server" SelectMethod="GetTagsByArticle"
                     TypeName="LD.Blog.Tag">
                     <SelectParameters>
                        <asp:Parameter Name="articleID" />
                     </SelectParameters>
                  </asp:ObjectDataSource>
               </p>
            </ItemTemplate>
            <LayoutTemplate>
               <asp:PlaceHolder ID="itemPlaceHolder1" runat="server" />
            </LayoutTemplate>
         </asp:ListView>
         <asp:ObjectDataSource ID="objArticles" runat="server" SelectMethod="GetArticles"
            TypeName="LD.Blog.Article" />
      </div>
   </form>
</body>
</html>

Figure 6

You see that this new ObjectDataSource (we've named it objTags) uses the GetTagsByArticle method, which requires articleID as an argument, as indicated by the <SelectParameters> collection. Now, we just need a way to get the ArticleID of each article being bound, and pass it to the parameter.

The underlying data object that a ListViewItem object is bound to is contained in the ListViewDataItem.DataItem property. You need access to this property in order to get the ArticleID you need to pass as an argument. Now, according to the MDSN, the DataItem property is only available during and after the ItemDataBound event of a ListView control. That isn't exactly true, as DataItem is also available during the ItemCreated event. (I suspect that this functionality may have been added at the last minute, and the MSDN docs weren't updated to reflect the change. Hey, stuff like that happens sometimes.) Anyway, for this example it doesn't really matter; we can use either event. So, let's use ItemDataBound.

protected void lvArticles_ItemDataBound(object sender, ListViewItemEventArgs e)
{
   if (e.Item.ItemType == ListViewItemType.DataItem)
   {
      ListViewDataItem dataItem = (ListViewDataItem)e.Item;
      Article article = (Article)dataItem.DataItem;
      ObjectDataSource objTags = (ObjectDataSource)e.Item.FindControl("objTags");
      Parameter parameter = objTags.SelectParameters[0];
      parameter.DefaultValue = article.ArticleID.ToString();
   }
}

Figure 7

You can see what's happening in Figure 7. As each item in lvArticles is bound, a reference to the item is obtained using the ListViewItemEventArgs.Item property. If that item is of type DataItem, it's cast to type ListViewDataItem. Then, the DataItem property of this ListViewDataItem is cast to type Article. A reference to the nested ObjectDataSource is obtained, the parameter is plucked from its SelectParameters collection, and the DefaultValue of the parameter is set to the ArticleID of the article just bound. Once you run this, you should see the output shown in Figure 1. And we're done!

Piece of cake, right? Well, as they say on those late-night infomercials: But wait! There's more! Let's investigate a different way to do this that doesn't involve a second ObjectDataSource, or even require us to handle any events.

Solution 2: Using a Databinding Expression

As the old saying goes, there's more than one way to skin a cat -- just like there's more than one way to bind a control to data. Instead of using DataSourceID to wire up to a data source control, you can use DataSource to bind directly to an data object. You can set the DataSource property in code, or you can set it declaratively using a databinding expression.

<%@ Page Language="C#" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
   <title>Articles - Nested ListView, Databinding Expression (Direct)</title>
</head>
<body>
   <form id="form1" runat="server">
      <div>
         <asp:ListView ID="lvArticles" runat="server" DataSourceID="objArticles"
            ItemPlaceholderID="itemPlaceHolder1">
            <ItemTemplate>
               <p>
                  <asp:Label ID="lblArticleTitle" runat="server" Font-Bold="true"
                     Text='<%# Eval("Title") %>' />
                  <br />
                  <asp:Label ID="lblPublishedDate" runat="server" Font-Italic="true"
                     Text='<%# Eval("PublishedDate","{0:MMM d, yyyy}") %>' />
                  <br />
                  Tags:
                  <asp:ListView ID="lvTags" runat="server" ItemPlaceholderID="itemPlaceHolder2"
                     DataSource='<%# LD.Blog.Tag.GetTagsByArticle((int)Eval("ArticleID")) %>'>
                     <ItemTemplate>
                        <asp:Label ID="lblTagName" runat="server" Text='<%# Eval("TagName") %>' />
                     </ItemTemplate>
                     <LayoutTemplate>
                        <asp:PlaceHolder ID="itemPlaceHolder2" runat="server" />
                     </LayoutTemplate>
                  </asp:ListView>
               </p>
            </ItemTemplate>
            <LayoutTemplate>
               <asp:PlaceHolder ID="itemPlaceHolder1" runat="server" />
            </LayoutTemplate>
         </asp:ListView>
         <asp:ObjectDataSource ID="objArticles" runat="server" SelectMethod="GetArticles"
            TypeName="LD.Blog.Article" />
      </div>
   </form>
</body>
</html>

Figure 8

Here, lvTags.DataSource uses a databinding expression to call the LD.Blog.GetTagsByArticle method directly, passing in an ArticleID obtained via the Eval method. More than likely however, you'd encapsulate this call into a property of your Article class. So let's go ahead and define a property that does just that:

using System;
using System.Collections.Generic;
 
namespace LD.Blog
{
   public class Article
   {
      public int ArticleID { get; set; }
      public string Title { get; set; }
      public DateTime PublishedDate { get; set; }
 
      private IEnumerable<Tag> tags;
      public IEnumerable<Tag> Tags
      {
         get
         {
            if (tags == null)
            {
               tags = Tag.GetTagsByArticle(this.ArticleID);
            }
            return tags;
         }
      }
 
      public static IEnumerable<Article> GetArticles()
      {
         // data access code not shown
      }
   }
}

Figure 9

In Figure 9, we've added a property to the Article class called Tags which contains the tags associated with an article. This lets us simplify the databinding expression a little bit:

<%@ Page Language="C#" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
   <title>Articles - Nested ListView, Databinding Expression (Property)</title>
</head>
<body>
   <form id="form1" runat="server">
      <div>
         <asp:ListView ID="lvArticles" runat="server" DataSourceID="objArticles"
            ItemPlaceholderID="itemPlaceHolder1">
            <ItemTemplate>
               <p>
                  <asp:Label ID="lblArticleTitle" runat="server" Font-Bold="true"
                     Text='<%# Eval("Title") %>' />
                  <br />
                  <asp:Label ID="lblPublishedDate" runat="server" Font-Italic="true"
                     Text='<%# Eval("PublishedDate","{0:MMM d, yyyy}") %>' />
                  <br />
                  Tags:
                  <asp:ListView ID="lvTags" runat="server" ItemPlaceholderID="itemPlaceHolder2"
                     DataSource='<%# Eval("Tags") %>' >
                     <ItemTemplate>
                        <asp:Label ID="lblTagName" runat="server" Text='<%# Eval("TagName") %>' />
                     </ItemTemplate>
                     <LayoutTemplate>
                        <asp:PlaceHolder ID="itemPlaceHolder2" runat="server" />
                     </LayoutTemplate>
                  </asp:ListView>
               </p>
            </ItemTemplate>
            <LayoutTemplate>
               <asp:PlaceHolder ID="itemPlaceHolder1" runat="server" />
            </LayoutTemplate>
         </asp:ListView>
         <asp:ObjectDataSource ID="objArticles" runat="server" SelectMethod="GetArticles"
            TypeName="LD.Blog.Article" />
      </div>
   </form>
</body>
</html>

Figure 10

If you run the page in Figure 10 using the same data, you'll see the results are identical to those obtained in the first solution, as shown in Figure 1.

Nesting Other Databound Controls in the ListView

Of course, these techniques aren't at all limited to nested ListViews. You can use them to bind any databound control inside a ListView. For example, we can show the tags as an unordered list using the BulletedList control.

<%@ Page Language="C#" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
   <title>Articles - Nested BulletedList, Databinding Expression
   </title>
</head>
<body>
   <form id="form1" runat="server">
      <div>
         <asp:ListView ID="lvArticles" runat="server" DataSourceID="objArticles"
            ItemPlaceholderID="itemPlaceHolder1">
            <ItemTemplate>
               <p>
                  <asp:Label ID="lblArticleTitle" runat="server" Font-Bold="true"
                     Text='<%# Eval("Title") %>' />
                  <br />
                  <asp:Label ID="lblPublishedDate" runat="server" Font-Italic="true"
                     Text='<%# Eval("PublishedDate","{0:MMM d, yyyy}") %>' />
                  <br />
                  Tags:
                  <asp:BulletedList ID="blTags" runat="server" DataSource='<%# Eval("Tags") %>'
                     DataTextField="TagName" />
               </p>
            </ItemTemplate>
            <LayoutTemplate>
               <asp:PlaceHolder ID="itemPlaceHolder1" runat="server" />
            </LayoutTemplate>
         </asp:ListView>
         <asp:ObjectDataSource ID="objArticles" runat="server" SelectMethod="GetArticles"
            TypeName="LD.Blog.Article" />
      </div>
   </form>
</body>
</html>

Figure 11

Using the same data, this yields the following output:

image

Figure 12

As you can see, nesting databound controls inside the ListView, including other ListViews, isn't difficult at all. Hope this helps some of you out there put the ListView to good use. Happy databinding!

EDIT: A couple of readers have asked for the source code for this post, so here you go.

Subscribe to this blog for more cool content like this!

Tweet this blog post

kick it on DotNetKicks.com

shout it on DotNetShoutOut.com

vote it on WebDevVote.com

Bookmark / Share

    » Similar Posts

    1. Master / Detail Editing With a ListView and DetailsView
    2. Resetting the Page Index in a ListView
    3. Building a TweetThis Extension with Automatic Bit.ly for Graffiti CMS

    » Trackbacks & Pingbacks

    1. You've been kicked (a good thing) - Trackback from DotNetKicks.com

    Trackback link for this post:
    http://leedumond.com/trackback.ashx?id=42

    » Comments

    1. Mark Nordin avatar

      This is one of the best articles on using the List view control; I really appreciate you taking the time to put this out. If you put out a book. I would be the first in line to buy it!

      Mark Nordin — March 17, 2009 4:59 PM
    2. Lee Dumond avatar

      Thanks! If you really like the writing, be sure to let my boss at Wrox Publishing know by posting a reply to this thread on the Wrox programmer forums. ;-)

      Lee Dumond — March 17, 2009 5:05 PM
    3. Greg Burman avatar

      Great stuff ! Goodbye Gridview! I am working on an Ajax project and was struggling with exactly this - I still have to figure out how to bind the nested ListViews on-demand (any ideas?) but this is a great start!

      Greg

      PS - big Wrox fan!

      Greg Burman — April 21, 2009 7:49 PM
    4. Marius C. avatar

      Now that's an article well written, nothing trivious, but is a perfect sample on how should make people understand differences.

      Good work,

      Marius C.

      Marius C. — April 22, 2009 6:15 AM
    5. Tony D. avatar

      Is there a way to download the sourec code for this example with the data access code.

      Tony D. — September 1, 2009 2:52 PM
    6. Lee Dumond avatar

      Tony -- not sure why the data access code matters, but let me see if I can dig it up and zip it to you.

      Lee Dumond — September 1, 2009 4:05 PM
    7. Tony D. avatar

      I am still learning, so I apologize in advance. What I am confused about is how does the objectdatasource get the ArticleID. If you have a static method, I am not sure how to pass the articleID to the public field ArticleID. Thank you for responding to my comment. I am sure you are busy so I appreciate your time.

      Thanks

      Tony D. — September 2, 2009 8:23 AM
    8. Lee Dumond avatar

      Tony: the "passing" of articleID takes place in the lvArticle_ItemDataBound event, which fires for each item in the list. First, the article being bound in each item is determined. Then, the value of the SelectParameter of the nested ODS is set to the ArticleID of the article found. The ODS then takes care of passing this parameter to its Select method.

      I have now provided a link to the source code at the end of the post.

      Lee Dumond — September 3, 2009 4:18 PM
    9. Tom avatar

      How would you take that example and add update / insert /delete editing capability to it. With all the binding pointing to objects and the object refs handling the fetching...how do we then wire up our save (both new and edit) and delete methods of the objects preserving the relationship. Meaning let the user delete a tag, individually. Also, if a user deletes an article all the tags should go as well. Assuming we have the methods all ready...how do wire up? This article is fantastic, this would make it incredible

      Tom — November 12, 2009 7:56 PM

    » Leave a Comment