Saturday, April 03, 2010

ASP.Net – Editing items in a nested DataList

I had a complex business entity that was setup with a hierarchical relationship (i.e. the parent object contained a list of child objects). I wanted to find a way to display the data such that users could edit items in the contained list.

What I found was the DataList makes it easy to display hierarchically related data using the concept of nested data-lists (I am sure one will be able to use my technique to create any kind of nested databound table). There are quite a few examples of how create nested data-lists and data-grids, but what almost all the samples lacked was the how regarding the editing of the nested data. (the few that I found – just did not work).

First take a look at the implemented code: http://www.aggregatedintelligence.com/NestedDataList/CategoriesView.aspx

Here is how I got it to work:

First the sample data-objects:

image

Category is the parent object and it contains a list of Items.

The goal is to be able to display the data in the following format:

Category    
Name    
Description    
Item-Line1 Item-Line2 Amount
Item-Line1 Item-Line2 Amount
Category    
Name    
Description    
Item-Line1 Item-Line2 Amount
Item-Line1 Item-Line2 Amount

The first step is to drag a DataList onto the web-page. You then add an item template to display the fields from the parent object (Category in this case).

<ItemTemplate>
<div class="divCategoryInfo">
<div>
Category:
<asp:Label ID="lblFirstName" runat="server" Text='<%#Bind("CategoryName")%>'></asp:Label>
</div>
<div>
Description:
<asp:Label ID="lblLastName" runat="server" Text='<%# Bind("Description") %>'></asp:Label>
</div>
</div>
<div class="divCategoryItems">
<!-- insert items here -->
</div>
</ItemTemplate>

Notice the place holder for the category items, we will come back to that later.

For now, lets plumb up the DataList. In the code-behind, we will perform data-binding during the Page Load event. The databinding needs to be performed ONLY during direct page loads and not during a post-back.

Now for the nested DataList. We could insert a new DataList directly into the ItemTemplate shown above. While that option does work for displaying of the data, it does not allow the nested DataList to be edited. (What used to happen was the ItemEditing event used to fire, but the ItemUpdated never ever fired – which meant that I could never get my hands at the updated data values).

Instead, what I found was that if I put the DataList into a user-control and then dropped it into the parent data-list, it allowed all the events to fire properly. One complication with this method was how to provide the nested data to the inner DataList. Another complication was: during post-back, as data-binding does not occur automatically, how do we again get the correct inner data and manually databind it to the nested data-list?

So here are the basic parts that need to be setup on the DataList that is in the user-control:

First create the user-control.
Next drag a data-list on the user-control. Setup the datalist to display the data.

<asp:DataList ID="dlItems" runat="server" Width="100%" 
OnItemCommand="mySubListItemHandler"
onEditCommand="myListEditHandler"
onUpdateCommand="myListUpdateHandler"
onCancelCommand="myListCancelHandler" BorderStyle="Solid"
BorderWidth="1px" GridLines="Both" >
<HeaderTemplate>
<th></th><th>Line 1</th><th>Line 2</th><th>Amount</th><th>In Shopping Cart</th>
</HeaderTemplate>
<ItemTemplate>
<td>
<asp:LinkButton ID="Linkbutton3" runat="server" CommandName="AddToCart" Text='<%#GetCartTitle(Eval("InShoppingCart")) %>' />
<asp:LinkButton ID="Linkbutton1" runat="server" CommandName="Edit" Text="Edit" />
</td>
<td>
<asp:Label ID="lblFirstName" runat="server" Text='<%#Bind("Line1")%>'></asp:Label>
</td>
<td>
<asp:Label ID="Label1" runat="server" Text='<%#Bind("Line2")%>'></asp:Label>
</td>
<td align="right">
<asp:Label ID="Label2" runat="server" Text='<%#Bind("Amount")%>'></asp:Label>
</td>
<td align="center">
<asp:ImageButton ID="ImageButton1" runat="server" CommandName="AddToCart"
ImageUrl='<%#GetImageUrl(Eval("InShoppingCart")) %>' />
</td>
</ItemTemplate>
<EditItemTemplate>
<td>
<asp:LinkButton ID="Linkbutton1" runat="server" CommandName="Update" Text="Update" />
<asp:LinkButton ID="Linkbutton2" runat="server" CommandName="Cancel" Text="Cancel" />
</td>
<td>
<asp:Label ID="lblFirstName" runat="server" Text='<%#Bind("Line1")%>'></asp:Label>
</td>
<td>
<asp:Label ID="Label1" runat="server" Text='<%#Bind("Line2")%>'></asp:Label>
</td>
<td>
<asp:TextBox ID="txtAmount" runat="server" Text='<%#Bind("Amount")%>'></asp:TextBox>
</td>
<td align="center">
<asp:ImageButton ID="ImageButton1" runat="server" CommandName="AddToCart"
ImageUrl='<%#GetImageUrl(Eval("InShoppingCart")) %>' />
</td>
</EditItemTemplate>
</asp:DataList>

The first thing to realize is that I have implemented the handlers for OnItemCommand, OnEditCommand, OnUpdateCommand, OnCancelCommand. The OnItemCommand is strictly not needed, but I wanted to keep track of when a subitem had been clicked. The next thing to realize is that I have an edit template defined for this DataList. It has only one element setup to be edited. (the Amount column).

Now the code-behind. What I realized was that you can get the parent DataList’s item index using the following code:
((System.Web.UI.WebControls.DataListItem)(this.Parent)).ItemIndex;
Now all I needed to do is that when the main page got loaded, I had to store the data in the SessionState, this would give me access to the data in the nested user-control. In my case I stored in list of categories in “Session["list"]”. This meant that every time I needed to perform data-binding in the nested user-control, I had to get the ((System.Web.UI.WebControls.DataListItem)(this.Parent)).ItemIndex item from the Session[“list”] object.
As for when to data-bind: You need to databind in page-load event only if it is not a post-back event. Other times you need to data-bind are when any of the events on the data-list fire (onItemCommand, OnEditCommand, etc).

A little bit about the sample code. It is a sample. I created it quickly to make sure that I could do what I wanted it to do. So dont complain that I dont check for this and I dont check for that. This is not production quality code. The page_load of the categoriesview page is weird because I am using a single data-generation method. It is meant to mock data coming from a different page where I would be selecting the categories that I am interested in. Also, I added some extra handling to keep track of a shopping cart into which items were being added and removed. The items are loaded using a DataProvider which creates the data randomly. The page also uses the MS Ajax framework to make the experience of working with the cart smoother.

Some other things to note. The user-control has an event that the main page subscribes to, to get notified about when an item is added or removed to the cart. Also, the check-box that represent that cart state of an item are simple image-links. The url is dynamically changed during databinding.

You can download the sample code from: http://cid-fbe9049ba8229d5b.skydrive.live.com/self.aspx/Public/WebApp%2004-03-2010.zip

No comments: