Persisting the Scroll Position of Child DIVs Using MS AJAX

Introduction

Although the ScriptManager and UpdatePanel found in Microsoft AJAX do a good job of persisting your pages' scroll position during partial post back operations, you might be surprised to find out the same is not true for scrollable child DIVs contained within an UpdatePanel.

The PersistentScrollPosition control presented in this article seeks to remedy this issue by using a client-side behavior and ASP.NET server control implemented using Microsoft AJAX.

Background

Although it is certainly not my intention to review the internals of the UpdatePanel and PageRequestManager or implementing any of the client-side components (Sys.Component, Sys.UI,Behavior, Sys.UI.Control), a quick understanding can go a long way into understanding and resolving this particular problem. There are two key items to keep in mind for this control:

  1. Client-side components are disposed of and recreated during the partial post back lifecycle, so you can't use the control's instance to store any data you need to survive this.
  2. The HTML output of the UpdatePanel is completely replaced during a partial post back (assuming it was triggered) through the innerHTML property, which is why the scroll position problem exists in the first place.

Using the Code

For those who just want the solution, using the code is straightforward. The control has one property you need to set; it's named ControlToPersist. This is a string that takes the ID of the server-side container DIV (it must have runat="server").

<asp:UpdatePanel runat="server" ID="UpdatePanel"
                 UpdateMode="always">
<ContentTemplate>
<asp:Button runat="server" ID="btnPostBack" Text="Post Back"
            OnClick="btnPostBack_Click" />
<br />
<div style="width:590px;height:400px;overflow-y:scroll;
     overflow-x:hidden;" runat="server" id="persistMe">
<p>
Lorem ipsum dolor sit amet, consectetuer adipiscing elit...</p>

</div>
<mbc:PersistentScrollPosition runat="server" ID="psf1"
   ControlToPersist="persistMe" /> 
</ContentTemplate>
</asp:UpdatePanel>

Building the Control

The control consists of two parts, both of which for the most part are very "cookie cutter." On the server side, you inhert from Control, implement IScriptControl and INamingContainer, and create a HiddenField during initialization to store your scroll position in between partial post backs.

public class PersistentScrollPosition : Control, IScriptControl,
   INamingContainer
{
protected override void OnInit(EventArgs e)
   {
      base.OnInit(e);

      // Create hidden control for storage
      storage = new HiddenField();
      storage.ID = "storage";
      Controls.Add(storage);
   }
}

When creating the script descriptors for the client-side initiation, you pass through the scrollable DIV's ClientID as the controls ElementID and you pass in a reference to the HiddenField's DOM element by using the AddElementProprety method of the ScriptComponentDescriptor class.

public IEnumerable<scriptdescriptor /> GetScriptDescriptors()
{
   ScriptComponentDescriptor scd =
      new ScriptBehaviorDescriptor
      ("Mbccs.WebControls.PersistentScrollPosition", Control.ClientID);
   scd.AddElementProperty("storage", storage.ClientID);
   yield return scd;
}

On the client side, the control inherits from the Sys.UI.Behavior base class. Upon control intialization, it hooks into two events: The scroll DOM event of the DIV, and the EndRequest event of the Sys.WebForms.PageRequestManager class. The EndRequest event is when the scroll state is restored, but I'll get to that shortly.

initialize : function() {
   Mbccs.WebControls.PersistentScrollPosition.callBaseMethod(this,
      'initialize');

   this._scrollDelegate = Function.createDelegate(this,
      this._onScroll);
   this._endRequestDelegate = Function.createDelegate(this,
      this._onEndRequest);

   var prm = Sys.WebForms.PageRequestManager.getInstance();
   prm.add_endRequest(this._endRequestDelegate);

   $addHandler(this.get_element(), 'scroll', this._scrollDelegate);

}

When the DIV is scrolled, the x,y scroll position is serialized and stored in the HiddenField server-side control you created earlier.

_onScroll : function(e) {
   this._storage.value = 
      Sys.Serialization.JavaScriptSerializer.serialize(this.
         _getScrollPosition());
},

_getScrollPosition : function() {
   var el = this.get_element();

   if (el) {
      return {
         x: el.scrollLeft || 0,
         y: el.scrollTop || 0
      };
   }
}

To prevent nulls from floating around, the x,y coordinates are coerced into 0's if either the scrollLeft or scrollTop properties are null.

The EndRequest event is fired when "an asyncronous postback is finished and control has been returned to the browser," (http://asp.net/AJAX/Documentation/Live/ClientReference/Sys.WebForms/PageRequestManagerClass/default.aspx), so it's a perfect time to restore your scroll state.

_onEndRequest : function(sender, args) {
   var o = null;
   if(this._storage.value !== '')
      o = Sys.Serialization.JavaScriptSerializer.
         deserialize(this._storage.value);

   if (o) {
      var el = this.get_element();
      el.scrollLeft = o.x;
      el.scrollTop = o.y;
      this._storage.value = '';
   }
}

The dispose method usually isn't a place of any particular interest, but it's worthy of noting that if you need to unhook the DIV's scroll event prior to calling dispose on the base class.

dispose : function() {
   $removeHandler(this.get_element(), 'scroll', this._scrollDelegate);

   Mbccs.WebControls.PersistentScrollPosition.callBaseMethod(this,
      'dispose');

   var prm = Sys.WebForms.PageRequestManager.getInstance();
   prm.remove_endRequest(this._endRequestDelegate);

   delete this._endRequestDelegate;
   delete this._scrollDelegate;
}


About the Author

Steven Berkovitz

Steven is VP Development at MBC Computer Solutions Ltd. (http://www.mbccs.com), a Markham based company specializing in e-Business Application Development, e-Store Solutions, Managed Co-Location and Proactive IT services. Steven has over 10 years experience in software and hardware design and is experienced with a large array of platforms, technologies and languages. Steven also has 3 years experience in QA software development for the accounting industry.

Downloads

Comments

  • Good article...better wording?

    Posted by PeejAvery on 10/20/2007 11:06am

    I like the article. It is clear and well written.

    I think that the wording of...

    "To prevent null's from floating around, the x,y coordinates are coerced into 0's if either the scrollLeft or scrollTop properties are null."

    Could be better read...

    "Ternary logic sets the scrollLeft or scrollTop variables to 0 if a null value is encountered."

    • Thanks..

      Posted by sberkovitz on 10/20/2007 02:00pm

      Thanks for your input - I guess I was really liking the word "coerced" when I was writing the article :)

      Reply
    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Companies undertaking an IT project need to find the right balance between cost and functionality. It's important to start by determining whether to build a solution from scratch, buy an out-of-the-box solution, or a combination of both. In reality, most projects will require some system tailoring to meet business requirements. Decision-makers must understand how much software development is enough and craft a detailed implementation plan to ensure the project's success. This white paper examines the different …

  • This report outlines the future look of Forrester's solution for security and risk (S&R) executives working on building an identity and access management strategy for the extended enterprise. We designed this report to help you understand and navigate the major business and IT trends affecting identity and access management (IAM) during the next five years. IAM in 2012 has become a tool not just for security but also for business agility. Competitive challenges push businesses into the cloud and encourage …

Most Popular Programming Stories

More for Developers

RSS Feeds

Thanks for your registration, follow us on our social networks to keep up-to-date