Scroll Call
Q. When a control in a Web form generates a postback, ASP.NET scrolls back to the top of the page. Can you prevent this unwanted scrolling so a page retains its scroll position even after posting back to the server?
A. You bet. The scrolling isn’t really ASP.NET’s doing — at least not directly. It’s a consequence of the fact that posting back to the server causes a brand-new page to be generated and returned to the browser. It’s annoying to click on a date in a Calendar control and suddenly find yourself back at the top of the page. The page in Figure 1 demonstrates this behavior in spades. Scroll down and click on any of the controls at the bottom of the page, and — presto! — you go right back to the top.
<html>
<body>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
<form runat=”server”>
<asp:Button Text=”Test” RunAt=”server” /><br>
<asp:LinkButton Text=”Test” RunAt=”server” /><br>
<asp:Calendar RunAt=”server” />
</form>
</body>
</html>Figure 1. Clicking on any of the controls in the Web form above scrolls back to the top of the page because it generates a postback.
The solution is to save the scroll position before the postback occurs and restore it afterward. One way to do this is to replace the page’s <body> tag with these (many thanks to reader Shaun Walker for his contribution to the ensuing logic):
<%
if (Request[”__SCROLLPOS”] != null &&
Request[”__SCROLLPOS”] != String.Empty) {
int pos = Convert.ToInt32 (Request[”__SCROLLPOS”]);
Response.Write (”<body id=”theBody” ” +
“onscroll=”javascript:document.forms[0]” +
“.__SCROLLPOS.value = theBody.scrollTop;” ” +
“onload=”javascript:theBody.scrollTop=” +
pos + “;”>”);
}
else
Response.Write (”<body id=”theBody” ” +
“onscroll=”javascript:document.forms[0]” +
“.__SCROLLPOS.value = theBody.scrollTop;”<”);
%>You also need to add this statement somewhere (anywhere) between the <l;form runat=”server”> and </form> tags:
<input type=”hidden” name=”__SCROLLPOS” value=”" />
And if you don’t have one already, add this directive to the top of the page:
<%@ Page Language=”C#” %>
What do these statements do? When the page is fetched outside a postback, the C# script emits a <body> tag containing an onscroll attribute that tracks the current scroll position and records it in the hidden <input> control, named __SCROLLPOS. If you execute a View/Source command, you’ll see something like this:
<body id=”theBody” onscroll=
“javascript:document.forms[0].__SCROLLPOS.value =
theBody.scrollTop;”>When a postback occurs, the last scroll position accompanies the form’s postback data by virtue of having been assigned to the __SCROLLPOS control. The C# script responds by emitting a <body> tag, containing both an onscroll attribute and an onload attribute, that restores the page’s last scroll position:
<body id=”theBody” onscroll=
“javascript:document.forms[0].__SCROLLPOS.value =
theBody.scrollTop;”
onload=”javascript:theBody.scrollTop=615;”>To see for yourself, run the page in Figure 2. The new and improved page retains its scroll position no matter which control you click.
<%@ Page Language=”C#” %>
<html>
<%
if (Request[”__SCROLLPOS”] != null &&
Request[”__SCROLLPOS”] != String.Empty) {
int pos = Convert.ToInt32 (Request[”__SCROLLPOS”]);
Response.Write (”<body id=”theBody” ” +
“onscroll=”javascript:document.forms[0]” +
“.__SCROLLPOS.value = theBody.scrollTop;” ”
“onload=”javascript:theBody.scrollTop=” +
pos + “;”>”);
}
else
Response.Write (”<body id=”theBody” ” +
“onscroll=”javascript:document.forms[0]” +
“.__SCROLLPOS.value = theBody.scrollTop;”>”);
%>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<l;br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
Hello, world!<br>Hello, world!<br>Hello, world!<br>
<form runat=”server”>
<input type=”hidden” name=”__SCROLLPOS” value=”" />
<asp:Button Text=”Test” RunAt=”server” /><br>
<asp:LinkButton Text=”Test” RunAt=”server” /><br>
<asp:Calendar RunAt=”server” />
</form>
</body>
</html>Figure 2. This page retains its scroll position, even in the face of postbacks. Changes are highlighted in bold.
Now for the caveats. As presented, this code works with Internet Explorer but not Netscape Navigator. Because Navigator doesn’t support onscroll, you must take more extraordinary measures to retain the scroll position there. Also, the client-side script generated by my server-side script assumes the page contains only one form (or that the runat=”server” form is the first form on the page). This usually is a valid assumption for ASP.NET pages because a page can have only one runat=”server” form. But if your page contains multiple forms and the runat=”server” form isn’t the first one, you’ll need to assign the form an ID and replace document.forms[0] in my script with that ID.
Incidentally, you easily could wrap all that messy server-side script in a custom control and enable developers to eliminate unwanted scrolling by replacing a <body> tag with, say, an <asp:Body RunAt=”server”> tag and including the hidden <input> control in their forms. Because that’s simply too much fun for one person to contemplate, I’ll leave the implementation to you.
Finally, note that you also can eliminate unwanted scrolling by including this directive in an ASPX file:
<%@ Page SmartNavigation=”true” %>
This technique is far easier, but it works only with Internet Explorer 5.0 or higher and it’s incompatible with some Web controls.