Dynamically Loading ListView Templates

I received an email question recently from a customer asking how they could dynamically load the LayoutTemplate for a ListView from a UserControl. I banged my head on this one for a while and eventually mailed our internal ASP.NET alias asking if someone could help. Fortunately David Fowler could. Hats off to him - the code below is his solution.

Let's start with a basic ListView page that we want to convert to load the LayoutTemplate from a UserControl. Something like this will do:

 <%@ 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 runat="server"></head>
<body>
  <form id="form1" runat="server">
    <asp:ListView ID="ListView1" runat="server"
      DataSourceID="XmlDataSource1">
      <LayoutTemplate>
        <ul>
          <asp:PlaceHolder ID="itemPlaceholder" runat="server"></asp:PlaceHolder>
        </ul>
      </LayoutTemplate>
      <ItemTemplate>
        <li><%# Eval("id") %></li>
      </ItemTemplate>
    </asp:ListView>
    <asp:XmlDataSource ID="XmlDataSource1" runat="server"
      XPath="/catalog/book"
      DataFile="~/XMLFile.xml">
    </asp:XmlDataSource>
  </form>
</body>
</html>

We have an XmlDataSource (in my case pointing to the MSDN sample XML file) and I'm binding the book ids to list items in an unordered list using a ListView control. Pretty straightforward so far.

Imagine I refactored this a little to use a UserControl for the LayoutTemplate

 <%@ Control Language="C#" %>
<ul>
  <asp:PlaceHolder ID="itemPlaceholder" runat="server"></asp:PlaceHolder>
</ul>

and one for the ItemTemplate

 <%@ Control Language="C#" %>
<li>
  <%# Eval("id") %>
</li>

That means I could replace my original aspx file with one that looks like this

 <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default2.aspx.cs" Inherits="Default2" %>

<!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">
</head>
<body>
  <form id="form1" runat="server">
    <asp:ListView ID="ListView1" runat="server" 
      ItemPlaceholderID="MyLayout$itemPlaceholder"
      DataSourceID="XmlDataSource1">
    </asp:ListView>
    <asp:XmlDataSource ID="XmlDataSource1" runat="server" 
      XPath="/catalog/book"
      DataFile="~/XMLFile.xml">
    </asp:XmlDataSource>
  </form>
</body>
</html>

ie I've defined no LayoutTemplate or ItemTemplate - we'll load these dynamically. The only other change is I've specified an ItemPlaceholderID on the ListView. We'll see why in a second.

 using System;
using System.Web.UI;

public partial class Default2 : System.Web.UI.Page
{
  protected void Page_Load(object sender, EventArgs e)
  {
    ListView1.LayoutCreated += new EventHandler(ListView1_LayoutCreated);
    ListView1.LayoutTemplate = LoadTemplate("LayoutTemplate.ascx");
    ListView1.ItemTemplate = LoadTemplate("ItemTemplate.ascx");
  }

  void ListView1_LayoutCreated(object sender, EventArgs e)
  {
    //remove the layout template
    ListView1.Controls.RemoveAt(0);

    //recreate it
    Control newLayoutContainer = new Control();
    ListView1.LayoutTemplate.InstantiateIn(newLayoutContainer);
    var userControl = newLayoutContainer.Controls[0];
    userControl.ID = "MyLayout";
    ListView1.Controls.Add(newLayoutContainer);
  }
}

image If you just go ahead and load the templates from the UserControls, you'll get the yellow screen of death telling you "An item placeholder must be specified on ListView 'ListView1'. Specify an item placeholder by setting a control's ID property to "itemPlaceholder". The item placeholder control must also specify runat="server". " The ListView simply can't find the ItemPlaceholder.

The key here is that the UserControl is a naming container so the ListView can't find the ItemPlaceholderID because it's been modified to avoid naming clashes on the page. In the code above we recreate the LayoutTemplate and set the UserControl's ID to "MyLayout" allowing us to specify that the ItemPlaceholderID will be "MyLayout$itemPlaceholder".

You could reduce this by determining up front the "native" ID of the UserControl ("ctl01" in this case) and setting the ItemPlaceholderID property of the ListView to "ctl01$itemPlaceholder". This would allow you to dispense with the LayoutCreated handler altogether. Of course if your UserControl ID then changes, it's going to break.

Technorati Tags: asp.net,listview