I spent a couple of hours today looking over the ListView and DataPager controls in ASP.NET 3.5. Both controls sound interesting and both provide welcome new features to the ASP.NET control arsenal. Although I'm a little wary of having yet another list control (as if we haven't enough of those already between DataGrid, DataList, Repeater, GridView and now the ListView) the ListView control kind of combines functionality of all of these controls into one. Maybe the most important feature of this control is that like the Repeater control it provides much more control over the rendering process at the cost of more markup code in the page.

The ListView is a sort of hybrid between a DataGrid and Repeater that combines the free form templating of the Repeater with the editing features of the data grid. It looks interesting because it basically allows you much more control over the layout than a DataGrid does while still giving you many of the more advanced features of the data grid. The ListView doesn't support paging natively, so the DataPager serves as an external control to provide paging features. The advantage of a separate control is that it gives you much more control about what the pager looks like and where it can be placed on the page - anywhere basically. The Pager is essentially an extender control that extends the ListView with paging capabilities.

The ListView Control

Using the ListView for basic data display is very similar to using a Repeater. You have the ListView control and a set of templates that you can apply which include:

  • LayoutTemplate
  • ItemTemplate
  • AlternatingItemTemplate
  • SelectedItemTemplate
  • EmptyItemTemplate
  • EmptyDataTemplate (?)
  • ItemSeparatorTemplate
  • GroupTemplate
  • GroupSeparatorTemplate
  • EditItemTemplate
  • InsertItemTemplate

That's a lot of templates <s>. There are a few new ones here like GroupTemplate and InsertItemTemplate. I haven't figured out how GroupTemplate works, but InsertItemTemplate certainly is a nice addition so you can handle both editing and adding in the same space (previously with a DataGrid you had to insertion externally or insert data manually into a DataTable to display the empty row).

Playing around with this this control  feels  nice in that you have all the control in the world to make it do what you want for rendering  at the cost of a bit more markup code you have to create on your own. But personally I find that I struggle with data grid/grid view type issues anyway and think it's actually easier to put the layout together yourself and get REALLY what you want (case in point take Dan Wahlin's cool recent post trying to coerce a DataGrid into fixed non-scrolling headers) rather than wade through abstracted properties 5 controls deep in gridview layout. It's much easier to create custom layouts for both display and editing.

Basic Data Display with the ListView

So to start off think of the ListView as a Repeater on steroids - everything is template driven including a 'master' layout template which is the layout template. For example a simple view might look like this:

<asp:ListView ID="lvItems" runat="server" 
              DataSourceID="Data" 
              ItemContainerID="layoutTemplate" 
              DataKeyNames="Pk"
 >
    <Layouttemplate>                
        <div id="layoutTemplate" runat="server" />                        
    </Layouttemplate>        
        
    <ItemTemplate>
        <div class="itemdisplay">
        <b><%# Eval("Sku") %></b><br />
        <%# Eval("Abstract") %></div>
    </ItemTemplate>
</asp:ListView>

You have a LayoutTemplate which determines the main outer 'container'  that hosts the detail items. The layout template can be any control as long as it can be marked as a server control (runat="server") as the layoutTemplate item is. You need to specify this item in the ListView's ItemContainerID property which tells the ListView where to place the detail content. In the example above the LayoutTemplate is not terribly useful since it merely puts the detail inside of the <div>. But the Layout template can also serve as a wrapper that allows for more complex layouts like tables. For example, the following ListView configuration duplicate's Dan's sample of a fixed grid header on a table (minus the styles to keep the code compact):

<asp:ListView ID="lvItemsTable" runat="server" 
              DataSourceID="Data" 
              ItemContainerID="layoutTableTemplate" 
              DataKeyNames="Pk"              
 >
    <LayoutTemplate>
        <div class="blackborder"  style="overflow-y:auto;height:500px;width:800px;">
        <table cellpadding="5" >
        <thead style="position:relative;">
        <tr class="gridheader" style="height:30px;">
            <th style="position:relative">Sku</th><th style="position:relative">Abstract</th>
        </tr>
        </thead>
        <tbody id="layoutTableTemplate" runat="server" 
               style="height:470px;overflow:scroll;overflow-x:hidden;"></tbody>
        </table>
        </div>
    </LayoutTemplate>

    <ItemTemplate>
       <tr style="height:0px;">
           <td valign="top"><%# Eval("Sku") %> </td>
           <td valign="top"><%# Eval("Abstract")  %></td>
       </tr>
    </ItemTemplate>

</asp:ListView>        

Notice again how the layoutTableTemplate is set in this case to the TBODY section. The ItemTemplate is then generated *inside* of the the specified element and in this case the data inside is a bunch of rows.

Adding Paging

If you want to add Paging you have to use the new DataPager control. This control is a free standing control and what's nice about it is that you can put it anywhere on the page - it doesn't have to be directly visually associated with the control it's actually doing the paging. The pager also is an Inline element so it doesn't force any particular positioning on you - you can simply wrap it in a div to format it as you like.

To place a page I can use markup like this to put the pager anywhere on the page:

<div class="blockheader" style="padding:10px;text-align: right;">
    <asp:DataPager ID="Pager" runat="server"  
                   PagedControlID="lvItems" PageSize="5" >                       
        <Fields>
            <asp:numericpagerfield ButtonCount="10" NextPageText="..." 
                PreviousPageText="..." />
            <asp:nextpreviouspagerfield FirstPageText="First" LastPageText="Last" 
                NextPageText="Next" PreviousPageText="Previous" />
        </Fields>
    </asp:DataPager>
</div>

As you can see there are template fields inside of the DataPager so you can control the layout of the pager which is handy. The key to this control is the PagedControlID which points at a ListView (presumably any control that implements a paging interface in the future potentially). You specify a page size and that's all there's to it.

You can also stick the Pager control into the Layout Template if you choose so the Pager can be a direct part of the generated output.

The pager seems like a great idea at first - it definitely addresses the layout scenario: You get more flexibility with placement and display. However, in its current form the pager is pretty damn limited. It only works with the ListView - it sure would have been nice if it could also work with a Repeater or GridView control. But even more disappointing is that the DataPager relies on ViewState of both the Pager control and - more puzzling - of the ListView. Yup both HAVE TO HAVE Viewstate enabled for paging to work.

Further the Pager control mysteriously has no Paging Events and no SelectedPageIndex property so as far as I can tell you can't even 'manually' override the paging behavior if you do want to run without ViewState on for the ListView.

I was also hoping that the Pager would facilitate server side paging by somehow filtering the data returned from the server (using ROW OVER in Sql 2005 for example) to retrieve only the data that's actually displayed, but this is also not the case. In fact it seems that the pager simply uses the DataSource of the ListView and filters it as needed. And as with the DataGrid paging does not work against a DataReader, only with a DataGrid.

Note that one thing missing from the ListView is sorting. There's no way to organize data explicitly and you're basically on your own with that.

Editing and Adding Data to the ListView

The ListView also supports editing and adding of data via the EditItemTemplate and InsertItemTemplate. This functionality works very similar to the way it works in a GridView for editing, but it's all done with custom templates:

<asp:ListView ID="lvItems" runat="server" 
              DataSourceID="Data" 
              ItemContainerID="layoutTemplate" 
              DataKeyNames="Pk"
              InsertItemPosition="None"               
 >

    <Layouttemplate>                
        <div id="layoutTemplate" runat="server" />                        
    </Layouttemplate>        
        
    <ItemTemplate>
        <div class="itemdisplay">
        <b><%# Eval("Sku") %></b><br />
        <%# Eval("Abstract") %></div>
        
        <asp:Button ID="Button1" runat="server" CommandName="Edit" Text="Edit" />
        <asp:Button ID="Button2" runat="server" CommandName="Delete" Text="Delete" />
    </ItemTemplate>
    <AlternatingItemTemplate >
        <div class="itemdisplayalternate">
        <b><%# Eval("Sku") %></b><br />
        <%# Eval("Abstract") %></div>
        <asp:Button ID="Button1" runat="server" CommandName="Edit" Text="Edit" />
        <asp:Button ID="Button2" runat="server" CommandName="Delete" Text="Delete" />
    </AlternatingItemTemplate>
    <EditItemTemplate>
        <div class="gridalternate">
        Sku: <asp:TextBox runat="server" ID="txtSku" Text='<%# Bind("Sku") %>'></asp:TextBox>
        <br />
        Abstract: <asp:TextBox  runat="server" id="txtAbstract" Text='<%# Bind("Abstract") %>'></asp:TextBox>
        <br />
        <asp:Button ID="Button3" runat="server" CommandName="Update" Text="Update" />
        <asp:Button ID="Button4" runat="server"
                    CommandName="Cancel" Text="Cancel" /><br />
         </div>
    </EditItemTemplate>
    <InsertItemTemplate>
        <div style="background:Yellow">
        <asp:TextBox runat="server" ID="txtSku" Text='<%# Bind("Sku") %>'></asp:TextBox>
        <br />
        <asp:TextBox  runat="server" id="txtAbstract" Text='<%# Bind("Abstract") %>'></asp:TextBox>
        <br />
        </div>
        <asp:Button ID="Button3" runat="server" CommandName="Inser" Text="Insert" />
        <asp:Button ID="Button4" runat="server"
                    CommandName="Cancel" Text="Cancel" /><br />
    </InsertItemTemplate>            
</asp:ListView>

In this case I'm using a Sql Data Source (I was lazy <s>) and as long as your provide Insert and Update statements this is pretty much all that's needed. InsertItemTemplate is new for any list control and a welcome addition I think as it allows for a consistent edit interface. The Insertion point is controlled by a special property called InsertItemPosition which can be FirstItem, LastItem or None. Typically you'd set this to none, and then have a button on the form that sets it to First Item (or LastItem):

protected void btnAddItem_Click(object sender, EventArgs e)
{
    this.lvItems.InsertItemPosition = InsertItemPosition.FirstItem;
}

protected void lvItems_ItemCommand(object sender, ListViewCommandEventArgs e)
{
    if (e.CommandName == "Update")
    {
        TextBox tb = e.Item.FindControl("txtSku") as TextBox;
        this.lvItems.InsertItemPosition = InsertItemPosition.None;
        Response.Write(tb.Text);
    }
    if (e.CommandName == "Cancel")
    {
        this.lvItems.InsertItemPosition = InsertItemPosition.None;
    }   
    
}

You can then implement an OnItemCommand and whenever you update or cancel reset to InsertItemPosition.None.

The Item Command returns a ListViewDataItem which can be used to retrieve control values, but again mysteriously is missing a DataItemIndex (properties exist but they are not set) or other direct links back to the data item. I really wish that would be one simple way to return the proper data item without having to explicitly assign a command argument - I mean c'mon the control knows which item index is active, it knows what the Data Item and Data Keys are - why can't I get these values passed to me? Especially since there is a DataItemIndex property that doesn't get set (it's always 0).  This is one thing that really bugs me about any of the ASP.NET data controls... Grrr. <s>

Grouping

I haven't really figured out how to use grouping other than how to set up the template, which basically involves setting up the group container in the LayoutTemplate and defining the ItemContainer inside of the GroupTemplate:

    <Layouttemplate>                
        <div id="groupContainer" runat="server" >                                
        </div>
    </Layouttemplate> 
    <GroupTemplate>        
        <div class="blockheader" style="height:23px;padding:7px">Group Header:</div>
        <div id="layoutTemplate" runat="server" />                        
    </GroupTemplate>       

You can then set a GroupItemCount to a numeric value to specify when the grouping occurs. So you can basically do a group header every 5 items for example. I don't see any way however to dynamically change the group. Group changes based on counts are pretty limited and rule out even simple one to many scenarios. Hopefully I'm just missing something here and there's a way to change this.

Summary

The new ListView control is pretty cool overall though. I provides functionality that wasn't there before and for those of use that want more control over layout than say the gridView offers at the cost of a bit more markup creation it can be a pretty nice tool. If you use templates with Repeaters and Grids a lot the ListView will likely become a common tool in your toolbox, but it obviously requires a bit more markup to make it all happen. But it's pretty flexible in terms of being able to display and edit data consistently in one place and that's definitely nice. It's a nice addition to ASP.NET 3.5.