Monday, July 11, 2011

ListBox jumps selected item to top - solution

When using a ListBox on an ASP.NET form using AutoPostBack the ListBox does not behave as expected:  when the form is posted back to the browser, not only is the rest of page updated but the ListBox itself is re-drawn by scrolling so that the currently selected item is now at the top of the ListBox. Initially this just appears to be a benign quirk, but when continually using forms using a ListBox it becomes extremely irritating.

This short tutorial demonstrates the behavior of a ListBox without the AutoPostBox-which works as expected-the behavior when AutoPostBack is enabled-which does not-and a straightforward cure that restores the original, expected, funtionality. This tutorial is written using Visual Studio 2010 with C# but the code is simple.

Create a ASP.NET Web Application for C# and replace the body content of Default.aspx with a ListBox:

<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
    CodeBehind="Default.aspx.cs" Inherits="ListBoxProblem._Default" %>
<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
</asp:Content>
<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
    <asp:ListBox ID="ListBox1" runat="server" Width="417px"></asp:ListBox>
</asp:Content>

The ListBox can be populated from any database table where there are more items to display than can be seen on within the display portion of the ListBox. To keep things simple, the Page_Load event of Default.cs in this example is updated to populate the ListBox with a static list of fruit, but the way it is populated is unimportant. Notice that it is only populated when there is not a postback event so that the list is not recreated when an item is created.

        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                ListItem[] data = { new ListItem("Apple"), new ListItem("Orange"), new ListItem("Banana"), new ListItem("Pear"), new ListItem("Apricot"), new ListItem("Grape"), new ListItem("Peach") };
                ListBox1.Items.AddRange(data);
                ListBox1.DataBind();
            }
        }
Run the project and play with the ListBox. This is a populated Listbox that works just as expected. When clicking any item, it is highlighted and the ListBox remains scrolled at exactly where it was; the scrollbars work but whichever item was highlighted remains in the same place after clicking it.



Often when a ListBox item is clicked the item becomes a reference to more data and the page needs to be updated with information based upon the item being clicked.  In this example, there will be a TextBox which is to be updated with the name of the fruit that has been clicked. In the real world, more complex information will then be updated.

Add a new TextBox to this page which it to be updated by when the ListBox is clicked.  Using the ListBox properties set AutoPostback to True which ensures that there is a round trip to the server when an event is triggered. In the ListBox events, double-click on the SelectedIndexChange event. This will add the name of the handler to the ASP code and switch to the code-behind C# code.

    <asp:ListBox ID="ListBox1" runat="server" Width="417px" AutoPostBack="True" 
        onselectedindexchanged="ListBox1_SelectedIndexChanged"></asp:ListBox><br /><br />
    <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>

The SelectedIndexChanged event is automatically created in the C#  to run on the server. By adding a single line of code the TextBox can be updated with the selected value from the ListBox control.

        protected void ListBox1_SelectedIndexChanged(object sender, EventArgs e)
        {
            TextBox1.Text = ListBox1.SelectedValue;
        }

The expectation would be that the TextBox is updated and everything remained as before with the selected ListBox item remaining highlighted at the same position in the display window. Unfortunately, when the ListBox is clicked, not only is the TextBox updated but the item that has been selected within the TextBox is redisplayed at the top of the ListBox:

When it first happens, it is just a little surprising, but after a while most users find it extremely annoying. Fortunately, there is a simple fix by adding two UpdatePanels and the controlling ScriptManager to the web page. Each UpdatePanel controls an area of the web page that is to be updated following a postback event.

    <asp:ScriptManager ID="ScriptManager1" runat="server">
    </asp:ScriptManager>
    <asp:UpdatePanel ID="UpdatePanel1" runat="server" ChildrenAsTriggers="False" 
        UpdateMode="Conditional">
    <ContentTemplate>
        <asp:ListBox ID="ListBox1" runat="server" Width="417px" AutoPostBack="True" 
            onselectedindexchanged="ListBox1_SelectedIndexChanged"></asp:ListBox><br /><br />
    </ContentTemplate>
    </asp:UpdatePanel>
    <asp:UpdatePanel ID="UpdatePanel2" runat="server">
    <ContentTemplate>
        <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
    </ContentTemplate>
    </asp:UpdatePanel>

The ScriptManager is just the standard "magic" to make the UpdatePanels work and needs no adjustment. The first UpdatePanel is placed around just the ListBox and the second UpdatePanel around the controls that are to be updated (in this case the TextBox). If the project is built and run at this point, then it will perform identically to before the panels were added.

But the first UpdatePanel must then be modified with two properties: UpdateMode must be set to Conditional and ChildrenAsTriggers must be set to False.  What this does is ensure that when the ListBox, as a child of the first UpdatePanel, tries to trigger an update (the default mode), the UpdatePanel does not respond to the event and so the ListBox remains untouched. Effectively the UpdatePanel round the ListBox absorbs the SelectedIndexChanged event from its' child and because the UpdateMode is set to Conditional, the UpdatePanel round the ListBox is not updated. At the same time the ScriptManager passes the Update event onto the other UpdatePanel on the page and it is updated; thus, the TextBox contained in the second UpdatePanel is updated with the selected value.

So finally, the application is back to behaving as expected. The one caveat is that you cannot update the ListBox as part of any ListBox event and expect that the browser will be automatically updated with the changes to the ListBox.

7 comments:

  1. Thanks for the solution. However, this does not work for my case.
    - I have an EDIT button next to the listbox.
    - on click of this button, I fetch something from database and display a set of controls.
    - Autopostback of listbox is also needed because on selection of each item, I update a grid below

    Selecting an item from listbox doesnt scroll up, but when I click on the EDIT button, listbox scrolls up.

    Any suggesting is appreciated.
    (please note I cannot use javascript here)

    Thank you so much

    ReplyDelete
  2. Thank you, this worked perfect for an issue I was having with it jumping to the top.

    ReplyDelete
  3. hey. i know its been a long time since you posted it. but you don't know how your post saved my day today!

    thank you so much!

    ReplyDelete
  4. Phil, you are awesome!!! I've been searching for over a year looking to understand why my LISTBOX list would move (jump) to the top/bottom of the list. It was the ChildrenAsTriggers="false" that was needed to tell the panel not the update itself! Thank you!

    ReplyDelete
  5. Awesome to know that a post from seven years ago is still helping people!
    Thanks for your post

    ReplyDelete
  6. And yet again this post has helped someone. Thank you!

    ReplyDelete