Categories
Uncategorized

Saving viewstate in repeater with droplists

Viewstate

Viewstate is that wonderful feature of ASP.Net webforms which saves away the initial states of control so you don’t have to reload them on page cycle. This can have quite significant performance gains over reloading the controls, especially if they are loaded from a database.

But viewstate can also cause your page size to bloat massively. I have on occasion working on some pages with clients where the page size was 2 Megabytes. That was before images. Eeek.

Scenario

In this scenario I have a repeater with a variable number of items. Each item has a drop list which could contain several hundred items. The viewstate will become quite large in this situation because ASP.Net will store viewstate for each control.

Now you could just set the “enableviewstate=false” but what you will discover is that the selectedvalue will not be saved. Not helpful that.

Most of the solutions to this problem involve pre-loading a single droplist in the page init event. Not being a ASP.Net guru and having a situation where I was using a repeater containing droplists, I decided on another solution.

My Solution

My solution involves using htmlselect items and hiddenfields which are kept in sync with jQuery.

Here’s my ASPX design:


<asp:repeater ID="rptrItemSelection" runat="server" OnItemCommand="ItemSelection_ItemCommand"

OnItemDataBound="ItemSelection_ItemDataBound">

<HeaderTemplate>

<table id="ItemSelections">

<tr>

<th>Centre</th><th>Category</th><th>Retailer</th><th></th>

</tr>

</HeaderTemplate>

<FooterTemplate>

</table>

</FooterTemplate>

<ItemTemplate>

<tr>

<td>

<select id="lstCentre" runat="server" cnt="<%# Container.ItemIndex%>" class="centreDropList" enableviewstate="false"></select></td>

<input type="hidden" id="hdCentre" runat="server" class="centreHidden" cnt="<%# Container.ItemIndex%>"/>

<td>

<select id="lstCategory" runat="server" cnt="<%# Container.ItemIndex%>" class="categoryDropList" enableviewstate="false"></select></td>

<input type="hidden" id="hdCategory" runat="server" class="categoryHidden" cnt="<%# Container.ItemIndex%>"/>

<td>

<select id="lstRetailer" runat="server" cnt="<%# Container.ItemIndex%>" class="retailerDropList" enableviewstate="false"></select>

<input type="hidden" id="hdRetailer" runat="server" class="retailerHidden" cnt="<%# Container.ItemIndex%>"/>

</td>

<td style="width: 3em;">

<asp:Button ID="btnItemRemove" runat="server" Visible="<%# iif(Container.ItemIndex>0,True,false) %>" CommandArgument="<%# Container.ItemIndex %>"

CommandName="Delete" Text="-" ToolTip="Remove"/>

</td>

</tr>

</ItemTemplate>

</asp:repeater>

Simple enough eh?

You’ll note that I’ve added a attribute named cnt to each item. That’s so I can sync the hidden field to the droplist (select).  As well I’ve turned off viewstate on the droplists but not on the hiddenfields. This is critical to success with this technique.

The jQuery which I’ve built with this is


<script type="text/javascript">

$(function () {
/* these hidden fields cut down on the viewstate by not needing to store the options items. Does mean we need to reload the lists each time. */

   $(".retailerDropList").change(function () {
      var itemIndex = $(this).attr("cnt");
      $(".retailerHidden[cnt='" + itemIndex + "']").val($(this).val());
   });
   $(".centreDropList").change(function () {
      var itemIndex = $(this).attr("cnt");
      $(".centreHidden[cnt='" + itemIndex + "']").val($(this).val());
   });

   $(".categoryDropList").change(function () {
     var itemIndex = $(this).attr("cnt");
      $(".categoryHidden[cnt='" + itemIndex + "']").val($(this).val());
   });
});

You’ll see that on the change event I grab the cnt attribute from the droplist so I can then locate the proper hidden field, which has the same attribute and sync its value to that of the droplist.

For the backend I’m using a repeater and to dynamically add and remove items I’m first building a list of items and then assigning it as the datasource of the repeater.

</pre>
Class CentreItem
   Public CentreID As Integer
   Public CategoryID As Integer
   Public RetailerID As Integer
   Sub New()
      CentreID = -1
      CategoryID = -1
      RetailerID = -1
   End Sub
End Class

To populate the repeater you can do


'Fill datatables with sources for droplists

dim dtA as datatable = getDataforA()
dim dtB as datatable = getDataforB()
dim dtC as datatable = getDataforC()

Dim ItemsList As New List(Of CentreItem)
ItemsList.Add(New CentreItem)

rptrItemSelection.DataSource = ItemsList
rptrItemSelection.DataBind()

the itemdatabound event would then be like


Protected Sub ItemSelection_ItemDataBound(sender As Object, e As RepeaterItemEventArgs)
If e.Item.ItemType = ListItemType.AlternatingItem Or e.Item.ItemType = ListItemType.Item Then
Dim oCitem As CentreItem = CType(e.Item.DataItem, CentreItem)
Dim lstCentre As HtmlSelect = CType(e.Item.FindControl("lstCentre"), HtmlSelect)
Dim lstCategory As HtmlSelect = CType(e.Item.FindControl("lstCategory"), HtmlSelect)
Dim lstRetailer As HtmlSelect = CType(e.Item.FindControl("lstRetailer"), HtmlSelect)
Dim hdCentre As HtmlInputHidden = CType(e.Item.FindControl("hdCentre"), HtmlInputHidden)
Dim hdCategory As HtmlInputHidden = CType(e.Item.FindControl("hdCategory"), HtmlInputHidden)
Dim hdRetailer As HtmlInputHidden = CType(e.Item.FindControl("hdRetailer"), HtmlInputHidden)

'fill centre with centres and categories. If centre is <> -1 then fill in retailer filtering by
'category if it is also <> -1

lstCentre.DataSource = dtA
lstCentre.DataTextField = "CentreName"
lstCentre.DataValueField = "CentreID"
lstCentre.DataBind()
lstCentre.Items.Insert(0, New ListItem("All", -1))
lstCentre.Value = oCitem.CentreID
hdCentre.Value = oCitem.CentreID

lstCategory.DataSource = dtB
lstCategory.DataTextField = "Category"
lstCategory.DataValueField = "CategoryID"
lstCategory.DataBind()
lstCategory.Items.Insert(0, New ListItem("All", -1))
lstCategory.Value = oCitem.CategoryID
hdCategory.Value = oCitem.CategoryID

Dim dtRetailers As DataTable = dtC
lstRetailer.DataSource = dtRetailers
lstRetailer.DataTextField = "RetailerName"
lstRetailer.DataValueField = "RetailerID"
lstRetailer.DataBind()
lstRetailer.Items.Insert(0, New ListItem("All", -1))
lstRetailer.Value = oCitem.RetailerID
hdRetailer.Value = oCitem.RetailerID

End If
End Sub

So that loads up the repeater and the droplists. But now what happens when you get a page cycle, such as clicking on a button. Well first thing I do is capture the selections, make whatever changes I need to make, and then re-databind the repeater to reload the data. You’ll note that it is necessary to reload the items from the database, your database will need to be quick.


Protected Sub btnAddItemRow_Click(sender As Object, e As EventArgs)

Dim lstCentreItems As List(Of CentreItem)

For Each rptrItem As RepeaterItem In rptrItemSelection.Items
 Dim item As New CentreItem
 item.CentreID = CType(rptrItem.FindControl("hdCentre"), HtmlInputHidden).Value
 item.CategoryID = CType(rptrItem.FindControl("hdCategory"), HtmlInputHidden).Value
 item.RetailerID = CType(rptrItem.FindControl("hdRetailer"), HtmlInputHidden).Value
 lstCentreItems.Add(item)
 Next

Dim newItem As New CentreItem

lstCentreItems.Add(newItem)

'refill in the datatables as before

rptrItemSelection.DataSource = lstCentreItems
rptrItemSelection.DataBind()

In this case I have a button which the user uses to add a row to the repeater. First thing is to go through the repeater collecting the hidden value fields, which contain the user’s selections. Then do what we need to do – in this case add a new item. Then rebind the repeater which re-creates it.

Conclusion

This is a simple technique which has the benefit of reducing the viewstate to bare minimum. I hope you get something out of this.

I’m not a jQuery or ASP.Net guru, there maybe a better way to do this. Feel free to comment if improvements can be made.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.