Wednesday 30 May 2007

Maintaining scroll position in .NET 2.0 ListBox on PostBack

Warning: this is only a partially working solution due to some Javascript issues described (and solved) here.

A requirement I had was to maintain the scroll position of ListBoxes on PostBack. The only solution I could find was to get the scroll through Javascript (the scrollTop property of the select) and restore it on page load, however, that would have meant a lot of custom controls, not to mention lots of work, to which I am usually against.

So, I used a ControlAdapter! The ControlAdapter is something new to the NET 2.0 framework. The Control in 2.0 looks for a ControlAdapter and delegates the usual methods (like OnLoad,OnInit,Render,etc) to the adapter. You tell the site to use an adapter for a specific type of control and possibly a specific browser type (by using a browser file), and it uses that adapter for all of the controls of the selected type and also the ones inherited from them. To disallow the "adaptation" of your control, override ResolveAdapter to always return null.

Ok, the code!
C# code
///<summary>
/// This class saves the vertical scroll of listboxes
/// Set Attributes["resetScroll"] to something when you want to reset the scroll
///</summary>
public class ListBoxScrollAdapter : ControlAdapter
{
    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        if ((Page != null) && (Control is WebControl))
        {
            WebControl ctrl = (WebControl) Control;
            string scrollTop = Page.Request.Form[Control.ClientID + "_scrollTop"];
            ScriptManagerHelper.RegisterHiddenField(Page, Control.ClientID + "_scrollTop", scrollTop);
            string script =
                string.Format(
                    "var hf=document.getElementById('{0}_scrollTop');var lb=document.getElementById('{0}');if(hf&&lb) hf.value=lb.scrollTop;",
                    Control.ClientID);
            ScriptManagerHelper.RegisterOnSubmitStatement(Page, Page.GetType(), Control.UniqueID + "_saveScroll",
                                                          script);
            if (string.IsNullOrEmpty(ctrl.Attributes["resetScroll"]))
            {
                script =
                    string.Format(
                        "var hf=document.getElementById('{0}_scrollTop');var lb=document.getElementById('{0}');if(hf&&lb) lb.scrollTop=hf.value;",
                        Control.ClientID);
                ScriptManagerHelper.RegisterStartupScript(Page, Page.GetType(), Control.ClientID + "_restoreScroll",
                                                          script, true);
            } else
            {
                ctrl.Attributes["resetScroll"] = null;
            }
        }
    }
}


Browser file content<browsers>
<browser refID="Default">
<controlAdapters>
<adapter controlType ="System.Web.UI.WebControls.ListBox"
adapterType="Siderite.Web.WebAdapters.ListBoxScrollAdapter" />
</controlAdapters>
</browser>
</browsers>


Of course, you will ask me What is that ScriptManagerHelper? It's a little something that tries to get the ScriptManager class without having to reference the System.Web.Extensions library for Ajax. That means that if there is Ajax around, it will use ScriptManager.[method] and if it is not it will use ClientScript.[method]. To.Int(object) is obviously something that gets the integer value from a string.

There is another thing, at the beginning I've inherited this adapter from a WebControlAdapter, but it resulted in showing all the options in the select (all the items in the ListBox) with empty text. The value was set as well as the number of options. It might be because in WebControlAdapter the Render method looks like this:
protected internal override void Render(HtmlTextWriter writer)
{
  this.RenderBeginTag(writer);
  this.RenderContents(writer);
  this.RenderEndTag(writer);
}

instead of just calling the control Render method.

9 comments:

  1. hi,
    geat solution to this problem, i'm going to use it.

    ReplyDelete
  2. Let me know how it works out... If there is any problem , I will fix it.

    ReplyDelete
  3. I just spent all afternoon working on a similiar solution and when I finished, I had it at 1 line of code. Try this also (it's in VB, so replace the &'s and put a semi-colon in, you know the story), this needs to be run everytime so don't nest it in a IsPostBack block. Also, the if statement is required because on the initial load those objects don't exist yet... they will on subsequent postbacks.

    ScriptManager.RegisterClientScriptBlock(Page, btnAdd.GetType, "Reposition", "if (document.getElementById('lstPrograms')) { document.getElementById('lstPrograms').selectedIndex=" & lstPrograms.SelectedIndex & "; }", True)

    ReplyDelete
  4. Your solution works only for single selection listboxes. Also, it doesn't preserve the scroll, it only scrolls to the selected item.

    ReplyDelete
  5. You are correct on both of your statements. Our end goals were different, in my brain fog I thought they were closer, my apologies.

    In my case, they've already moved the contents from a left hand box to a right hand box (with a multi-select) on a post back and the only spec was to keep the scroll back on the first selected (I would prefer the last selected though at which point we could just loop through to find it's index).

    You are right though, my example does not preserve the multiple selections, just the first selected scroll position.

    I forgot to mention also, yours is a great solution, many thanks for sharing it. :)

    ReplyDelete
  6. this seems to work great in .net 3.5
    with this slight modification
    add declaration /assignment
    ClientScriptManager cs = Page.ClientScript;

    change references to scriptmanagerhelper to cs and remove page as parameter in the three cs. method calls

    that takes care of nonajax

    also to take care of items in update panals repeat the codeblock in another if statement
    (Page != null) && (Control is WebControl) && ScriptManager.GetCurrent(Page) != null)
    and in this codeblock replace scriptmanagerhelper with ScriptManager

    ReplyDelete
  7. Thanks very much!!!!
    solution to a big dilema...

    ReplyDelete
  8. First easily implemented solution I've found after A LOT of looking, many thanks!

    ReplyDelete