Monday, October 10, 2011
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 aListView. The tags ListView would be nested inside the articles ListView.
You might end up with something like the following:
Figure 1
How would you approach this? Well, binding the outer ListView (the articles) is not very tricky at all. You could use anObjectDataSource, 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 theGetArticles 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:
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 DataItemproperty 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 theListViewItemEventArgs.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 theDataSource 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 ArticleIDobtained 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 aListView. 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:
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!