Scott on Writing

Musings on technical writing...

Maintaining Scrollback Position Across Postbacks

Anyone who's created a large Web Form that entails a load of postbacks can atest to the user interface problem that occurs in such scenarios:

  • “Flashing“ of the UI elements, and
  • Loss of scroll position (namely, if you scroll down to the bottom of a page, and then interact with the UI as to cause a postback, the scroll position is lost)

One option to curing these ills is to use SmartNavigation.  SmartNavigation uses a hidden IFRAME to handle the postback, so the window being viewed by the user is not “refreshed,” per se, and therefore doesn't suffer from the flashing and loss of the scroll position.  There are, however, two main problems with SmartNavigation:

  1. It only works with IE 5.5+
  2. As others have noted, it seems to cause more harm than good.  There are a myriad of confusing, hard to diagnose errors that can creep up once SmartNavigation is turned on.

Recently Steve Stchur authored an article for 4Guys that demonstrated how to obtain scroll position on postback without using SmartNavigation.  In Maintaining Scroll Position on Postback, Steve introduces a custom server control that, once plopped down on a page, automatically maintains scroll position for the page on postback (see a live demo).  For a recent project of mine, I took this idea and tweaked it so that rather than using a server control, I extend the System.Web.UI.Page class, and have those pages that need to maintain scroll position on postback inherit from this extended class.  Here is the code for my extended Page class:

Imports System.text

Public Class SmartScollerPage
    Inherits Page

    Protected Overrides Sub OnPreRender(ByVal e As EventArgs)
        If Not Request.Form("scrollLeft") Is Nothing Then
            RegisterHiddenField("scrollLeft", Request.Form("scrollLeft"))
            RegisterHiddenField("scrollTop", Request.Form("scrollTop"))
        Else
            RegisterHiddenField("scrollLeft", String.Empty)
            RegisterHiddenField("scrollTop", String.Empty)
        End If

        Dim scrollScript As New StringBuilder(1000)
        scrollScript.Append("<script language = ""javascript"">")
        scrollScript.Append(String.Concat(vbCrLf, "<!--", vbCrLf))
        scrollScript.Append("function SmartScroller_GetCoords() {")
        scrollScript.Append(String.Concat(vbCrLf, "var scrollX, scrollY;", vbCrLf))
        scrollScript.Append("if (document.all) {")
        scrollScript.Append(String.Concat(vbCrLf, "if (!document.documentElement.scrollLeft)", vbCrLf))
        scrollScript.Append("scrollX = document.body.scrollLeft;")
        scrollScript.Append(String.Concat(vbCrLf, "else", vbCrLf))
        scrollScript.Append("scrollX = document.documentElement.scrollLeft;")
        scrollScript.Append(String.Concat(vbCrLf, "if (!document.documentElement.scrollTop)", vbCrLf))
        scrollScript.Append("scrollY = document.body.scrollTop;")
        scrollScript.Append(String.Concat(vbCrLf, "else", vbCrLf))
        scrollScript.Append("scrollY = document.documentElement.scrollTop; }")
        scrollScript.Append(String.Concat(vbCrLf, "else {", vbCrLf))
        scrollScript.Append("scrollX = window.pageXOffset; scrollY = window.pageYOffset; }")
        scrollScript.Append(String.Concat(vbCrLf, vbCrLf))
        scrollScript.Append("document.getElementById('scrollLeft').value = scrollX;")
        scrollScript.Append(vbCrLf)
        scrollScript.Append("document.getElementById('scrollTop').value = scrollY;")
        scrollScript.Append(String.Concat(vbCrLf, "}", vbCrLf))

        scrollScript.Append(String.Concat(vbCrLf, "function SmartScroller_Scroll() {", vbCrLf))
        scrollScript.Append("var x = document.getElementById('scrollLeft').value;")
        scrollScript.Append(String.Concat(vbCrLf, "var y = document.getElementById('scrollTop').value;", vbCrLf))
        scrollScript.Append("window.scrollTo(x, y); }")

        scrollScript.Append(String.Concat(vbCrLf, vbCrLf, "window.onload = SmartScroller_Scroll;"))
        scrollScript.Append(String.Concat(vbCrLf, "window.onscroll = SmartScroller_GetCoords;", vbCrLf))
        scrollScript.Append("window.onclick = SmartScroller_GetCoords; window.onkeypress = SmartScroller_GetCoords;")
        scrollScript.Append(String.Concat(vbCrLf, "// -->", vbCrLf))
        scrollScript.Append("</script>")

        RegisterClientScriptBlock("scrollCode", scrollScript.ToString())
    End Sub

End Class

If you compare my code to Steve's (which is written in C#), you'll notice a few subtle changes.  Rather than inject the code in the OnInit() method, as Steve does, I have mine emitted in the OnPreRender() method instead.  There's no reason the script can't be emitted in the Initialization stage, but typically client-side script is not generated until the PreRender stage, since typically the script's values could depend on properties/methods set/called by the page developer during stages after Initialization (but before PreRender). 

Also, Steve uses HtmlInputHidden controls to hold his values across postback.  This is probably a better approach, as these controls automatically hold their own view state.  Instead, I use the RegisterHiddenField() method to squirt out the hidden form fields directly.  Of course, in doing this I need to check the Request.Forms() collection and manually add in the values across postbacks (this is what the HtmlInputHidden control's do for us with their view state automagically).

Finally, in the client-side script to reference my hidden form fields I use document.getElementById() rather than document[formID].hiddenInputID, like Steve uses.  The benefit of my approach is that I don't need to get the form's ID, which Steve does in his control's code by recursing through the control hierarchy.  (For completeness, my extended Page class should have a call to VerifyRenderingInServerForm().)

Of course, all of this will be moot when ASP.NET 2.0 ships, but for those (like myself) still developing and deploying ASP.NET 1.x solutions, this code provides a better user interface by returning to the previous scroll position on postback.  Enjoy!

posted on Friday, November 19, 2004 12:07 PM

Feedback

# Retain scroll position in ASP.NET without the SmartNavigation non-feature - level 200 11/22/2004 6:36 AM Jeffrey Palermo

Retain scroll position in ASP.NET without the SmartNavigation non-feature - level 200

# What's Cool for November 22 11/22/2004 1:43 PM OdeToCode Link Blog

# re: Maintaining Scrollback Position Across Postbacks 11/30/2004 8:36 PM Chris Wertman

Well , unfortunatley your code does not work in FireFox/Mozilla , works Ok in IE but fails there and his (which appears not even to be his) Any Ideas I cant get ANY of this working under .Net 2.0 (Yours is the closest but only on IE :(

# re: Maintaining Scrollback Position Across Postbacks 12/5/2004 11:00 PM dh

Two things, two get this to work on FireFox and Netscape.

Neither like the document.getElementById to find the scrollLeft and scrollTop objects, so you have to replace that code with a call to a cross browser compatible function to find the object (I used the javascript snippet from dreamweaver but I'm sure there are others) like this:
old cold
document.getElementById('scrollLeft').value = scrollX
document.getElementById('scrollTop').value = scrollY

new code:
var o; o = findObj('scrollLeft');
o.value = scrollX;
o = findObj('scrollTop');
o.value = scrollY;


The second problem that took much longer to find for Netscape 7.0 was the window.onscroll call. Netscape choked on this, so you have to do a test for IE before calling the SmartScroller_GetCoords() function. Like this:

window.onscroll = test;

function test()
{
if (document.all ) SmartScroller_GetCoords();
}

all is working great for me in IE6, IE5.5, Netscape 7.0 and Firefox 1.0 - thanks for the great script.

don

# re: Maintaining Scrollback Position Across Postbacks 2/1/2005 7:54 AM Vadeem

This is a great class, Thank You.

# re: Maintaining Scrollback Position Across Postbacks 2/9/2005 3:42 AM stephg

Well, this is not working for datagrids that are netsted in <DIV> </DIV>

# re: Maintaining Scrollback Position Across Postbacks 2/9/2005 5:40 PM Jesse

So far none of these options work, so in order to do this properly, I took the original Server Control code, and made a Page base class out of it. The only tweak to get it working properly in Firefox is to add the HtmlInputHidden to the Form control, not the Page. if you use the RegisterHiddenField method, your page will not work in other browsers because that method does not set the ID attribute on the hidden input element. Therefore the getElementById will fail. by using the explicit DOM path for the hidden controls in Scott's code you get a much more browser compatible script. Here's the revised code:

public class SmartScrollerPage : System.Web.UI.Page {
private HtmlForm m_theForm = new HtmlForm();

private HtmlForm GetServerForm(ControlCollection parent) {
foreach (Control child in parent) {
Type t = child.GetType();
if (t == typeof(System.Web.UI.HtmlControls.HtmlForm))
return (HtmlForm)child;

if (child.HasControls())
return GetServerForm(child.Controls);
}

return new HtmlForm();
}

protected override void OnPreRender (EventArgs e) {

m_theForm = GetServerForm(Page.Controls);

string topValue = Request.Form["scrollTop"];
string leftValue = Request.Form["scrollLeft"];

if (topValue == null || leftValue == null){
topValue = leftValue = "0";
}

HtmlInputHidden hdnLeft = new HtmlInputHidden();
hdnLeft.ID = "scrollLeft";
hdnLeft.Name = "scrollLeft";
hdnLeft.Value = leftValue;
m_theForm.Controls.Add(hdnLeft);


HtmlInputHidden hdnTop = new HtmlInputHidden();
hdnTop.ID = "scrollTop";
hdnTop.Name = "scrollTop";
hdnTop.Value = topValue;
m_theForm.Controls.Add(hdnTop);

string scriptString = @"
<!-- SmartScroller ASP.NET Generated Code -->
<script language = ""javascript"">
<!--
function SmartScroller_GetCoords()
{
var scrollX, scrollY;
if (document.all)
{
if (!document.documentElement.scrollLeft)
scrollX = document.body.scrollLeft;
else
scrollX = document.documentElement.scrollLeft;

if (!document.documentElement.scrollTop)
scrollY = document.body.scrollTop;
else
scrollY = document.documentElement.scrollTop;
}
else
{
scrollX = window.pageXOffset;
scrollY = window.pageYOffset;
}

document.forms[""" + m_theForm.ClientID + @"""]." + hdnLeft.ClientID + @".value = scrollX;
document.forms[""" + m_theForm.ClientID + @"""]." + hdnTop.ClientID + @".value = scrollY;

}


function SmartScroller_Scroll()
{

var x = document.forms[""" + m_theForm.ClientID + @"""]." + hdnLeft.ClientID + @".value;
var y = document.forms[""" + m_theForm.ClientID + @"""]." + hdnTop.ClientID + @".value;

window.scrollTo(x, y);
}


window.onload = SmartScroller_Scroll;
window.onscroll = SmartScroller_GetCoords;
window.onclick = SmartScroller_GetCoords;
window.onkeypress = SmartScroller_GetCoords;
// -->
</script>
<!-- End SmartScroller ASP.NET Generated Code -->";

Page.RegisterStartupScript("SmartScroller", scriptString);
}
}

# re: Maintaining Scrollback Position Across Postbacks 3/22/2005 3:39 PM Bob Kenmotsu

Hello,

My .ascx.vb page is already inheriting from another code base, and VS tells me only one base at a time can be inherited.

Can your code be called through an 'imports' and then calling 'OnPreRender'?

Thanks,

Bob

# re: Maintaining Scrollback Position Across Postbacks 6/20/2005 2:33 PM Shah

I like your solution. However, your script would look better if you handled window.onclick & window.onkeypress events more reasonably. What if there's already a handler assigned for those events?

# re: Maintaining Scrollback Position Across Postbacks 8/4/2005 9:36 AM Galin Iliev -

hello Scott,
I have problems with this script because of line
window.onload = SmartScroller_Scroll;

I have problem with my website menu - I am not pretty sure why as one of my team wrote this

Is there any elegant way to attach eventhadler in events collections

# re: Maintaining Scrollback Position Across Postbacks 9/30/2005 4:24 AM Sandra Watt

I just recently got around this. You need a JavaScript function that will hook an additional onload event, rather than just replacing. See:

http://www.scottandrew.com/weblog/articles/cbs-events

Scott's solution worked for me.

# re: Maintaining Scrollback Position Across Postbacks 3/1/2006 9:35 PM cayce

For FireFox try using var elements = document.getElementsByName which returns a node list. Afterwards, access the elements.items[0] element. The problem, I'am almost certain, is RegisterHiddenField does not put an ID on the hidden field, only a name. This is why document.getElementById will not find the hidden field client side.

# re: Maintaining Scrollback Position Across Postbacks 3/20/2006 9:29 AM T Parker

This code is great and the server control is so convenient to use.

However, in my app, when the control invokes to restore scroll position, it is not painting the browser completely. There is an "image" of the controls at the top of the page overlaying the controls at the current scroll position. If you scroll down and back up, the image is gone...any idea how to make the screen completely refresh when the server control restores the scroll position?

# re: Maintaining Scrollback Position Across Postbacks 3/29/2006 8:43 AM AJ

The Code works great.
Thanks

Title:  
Name:  
Url:
Protected by Clearscreen.SharpHIPEnter the code you see:
Comments   

Add To Your Reader

My Links

Archives

Post Categories

 

I am a Microsoft MVP for ASP.NET.
I am an ASPInsider.
<May 2008>
SMTWTFS
27282930123
45678910
11121314151617
18192021222324
25262728293031
1234567

Comment Stats

DayTotal% of Total
Sunday 1866.8%
Monday 37913.9%
Tuesday 45316.7%
Wednesday 50418.5%
Thursday 53519.7%
Friday 49418.2%
Saturday 1666.1%
Total 2717100.0%

Hour1Total% of Total
12:00 AM 652.4%
1:00 AM 682.5%
2:00 AM 622.3%
3:00 AM 742.7%
4:00 AM 572.1%
5:00 AM 1033.8%
6:00 AM 1084.0%
7:00 AM 1585.8%
8:00 AM 1716.3%
9:00 AM 1475.4%
10:00 AM 1716.3%
11:00 AM 1816.7%
12:00 PM 1886.9%
1:00 PM 1696.2%
2:00 PM 1605.9%
3:00 PM 1324.9%
4:00 PM 1073.9%
5:00 PM 923.4%
6:00 PM 913.3%
7:00 PM 963.5%
8:00 PM 833.1%
9:00 PM 782.9%
10:00 PM 792.9%
11:00 PM 772.8%
Total 2717100.0%

Comments by Blog Entry Date/Time

Day Entry MadeAvg.Total
Sunday 5.54144
Monday 5.22339
Tuesday 4.28419
Wednesday 7.67637
Thursday 6.90607
Friday 5.48411
Saturday 5.33160
Total 5.842717

Hour1 Entry MadeAvg.Total
12:00 AM 5.0035
1:00 AM 1.002
5:00 AM 0.000
7:00 AM 7.0035
8:00 AM 5.35107
9:00 AM 6.32278
10:00 AM 6.47246
11:00 AM 4.41181
12:00 PM 6.88330
1:00 PM 3.00111
2:00 PM 5.41222
3:00 PM 8.64285
4:00 PM 4.0589
5:00 PM 5.92154
6:00 PM 4.52113
7:00 PM 9.67174
8:00 PM 9.80147
9:00 PM 5.05111
10:00 PM 5.4265
11:00 PM 4.5732
Total 5.842717

Learn More About Comment Stats
1 - All times GMT -8...


Blog Stats

Favorite Web Sites

My Books

My MSDN Articles