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.