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:
- It only works with IE 5.5+
- 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!