Tip

Scroll call

Please let other users know how useful this tip is by rating it below. Do you have a tip or code of your own you'd like to share? Submit it here.


Prevent unwanted scrolling, make scrollable DataGrids and learn why paranoia can be a good thing.

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.


Q. Pageable DataGrids are cool. But what about scrolling DataGrids? Are there any simple tricks I can use to make a DataGrid scroll?

A. Is a <div> tag simple enough? The ASPX file in Figure 3 displays selected data from Northwind's Products table in a scrolling DataGrid. The DataGrid scrolls inside a 256 pixel-high panel defined by a <div> tag as shown in Figure 4. To change the height, simply modify the style attribute.

<%@ Import Namespace="System.Data.SqlClient" %>

<html>
  <body>
    <form runat="server">
      <div style="height: 256px; overflow: auto">
        <asp:DataGrid ID="MyDataGrid" Width="100%"
          RunAt="server" />
      </div>
    </form>
  </body>
</html>

<script language="C#" runat="server">
void Page_Load (Object sender, EventArgs e)
{
    if (!IsPostBack) {
        SqlConnection connection = new SqlConnection
             ("server=localhost;database=northwind;uid=sa");
        try {
            connection.Open ();
            SqlCommand command = new SqlCommand
                 ("select ProductName, UnitsInStock " +
                "from Products", connection);
            SqlDataReader reader =
                command.ExecuteReader ();
            MyDataGrid.DataSource = reader;
            MyDataGrid.DataBind ();
        }
        finally {
            connection.Close ();
        }
    }
}
</script>
Figure 3. What do you get when you combine a DataGrid and a <div>? A scrolling DataGrid!


Figure 4. Here's a scrolling DataGrid in action. You can dress up the DataGrid for greater visual effect.


Q. One of my colleagues -- I'll call him Bob -- recommended that to reduce the number of database accesses performed by an electronic storefront we're building in ASP.NET, we should round-trip information regarding catalog items in hidden <input> controls, like this:

<input type="hidden" name="ItemID" value="12345678">
<input type="hidden" name="Description" value="Monitor">
<input type="hidden" name="Price" value="1179.99">

Another member of the team -- Alice -- says this leaves us wide open to security breaches. Bob says Alice is paranoid and that ASP.NET uses hidden <input> controls all the time. Who's correct?

A. Pay attention to Alice -- she might be paranoid, but she's one smart cookie. And she'll probably save your client a lot of money -- not to mention your company a couple of lawsuits.

Here's the deal. Suppose you use hidden <input> controls as described to embed data in the Web page. When the form is submitted to the server, the resulting HTTP request will look something like this:

POST /monitorpage.html HTTP/1.1
  .
  .
  .
Content-Type: application/x-www-form-urlencoded
Content-Length: 50
ItemID=12345678&Description=Monitor&Price=1179.99

Presumably, you'll add the item to a shopping cart at the stated price. And by round-tripping the price to the client and back, you avoid the need to hit the database again to get the price of item 12345678.

It sounds good, but it's an accident waiting to happen. Even an unsophisticated hacker could spoof your application by submitting this HTTP request:

POST /monitorpage.html HTTP/1.1
  .
  .
  .
Content-Type: application/x-www-form-urlencoded
Content-Length: 48
ItemID=12345678&Description=Monitor&Price=11.99

Paying $11.99 for a $1,179.99 monitor is a pretty good deal. But if you don't validate the price on the server side, that's exactly what will happen. The world is full of people who patrol the Web looking for vulnerabilities like this one.

Does ASP.NET use hidden <input> controls? Yes. The most common example is ASP.NET's __VIEWSTATE control, which round-trips view state to the client and back. View state earns its name because it's used by controls such as DropDownList and DataGrid to persist their contents (that's why you don't have to reinitialize ASP.NET server controls inside postbacks). But view state can contain other data as well. In fact, pages (and controls) can store virtually anything they want in view state.

A malicious user can modify the value of __VIEWSTATE before posting back to the server. It follows that you probably don't want to store prices and other sensitive data in view state unless you intend to validate them on the server. There is, however, an alternative. You can make view state cryptographically tamper-proof by including this directive in an ASPX file:

<%@ Page EnableViewStateMac="true" %>

Or, you can include this statement in web.config:

<pages enableViewStateMac="true" />

"Mac" stands for "message authentication code." Setting EnableViewStateMac to true appends to the view state returned to the client a hash value computed from the combined view state and a secret key known only to the server. Upon postback, ASP.NET rehashes the view state transmitted in the request and rejects it if the new hash doesn't match the old. (Note: In version 1.0, ASP.NET's machine.config file sets enableViewStateMac to true, turning view state verification on by default. You still should enable verification explicitly, however, so changes to machine.config won't disable the tamper-proofing inadvertently - especially in hosting scenarios.)

Tamper-proofing prevents view state from being changed, but it doesn't prevent it from being read. If you write passwords or other ultra-sensitive values to view state and want to hide them from prying eyes, you should go one step further and ask ASP.NET to encrypt view state entirely. You do this by setting EnableViewStateMac to true and including this statement in web.config:

<machineKey validation="3DES" />

Armed with this knowledge, you can satisfy both Bob and Alice's objectives by storing item information in view state and configuring ASP.NET to encrypt it. In C#, this statement writes the item's price into view state:

ViewState["Price"] = 1179.99m;

This statement reads it back following a postback:

decimal price = (decimal) ViewState["Price"];

Bob is happy because the price is round-tripped in a hidden <input> control, and Alice is happy because it's round-tripped securely. Keep in mind, however, that when it comes to security, nothing beats keeping potentially injurious data on the server - even if that means performing additional database accesses.

Round-tripping data in view state is one way to minimize time-consuming database accesses, but ASP.NET offers other ways as well. In particular, the ASP.NET application cache is an ideal storage medium for retaining data between requests. Data stored in the application cache is available to all users of a site, and you can assign expiration policies to items stored in the cache that limit their lifetimes. You even can key a cached item's lifetime to that of other items or to file-system objects. The application cache is one of the most effective weapons you have for building high-performance Web applications. If you're not familiar with the application cache already, now's a great time to crack open the documentation and learn about it.

Jeff Prosise is author of several books, including "Programming Microsoft .NET" (Microsoft Press). He also is a co-founder of Wintellect (http://www.wintellect.com), a software consulting and education firm that specializes in .NET. Got a question for this column? Submit queries to mailto:askthepro@aspnetPRO.com.


This article was provided by asp.netPRO Magazine, an online information resource for the ASP.NET developer community, which helps professional software developers build, deploy and run the next generation of dynamic, distributed Web applications quickly and easily. Click here to learn more.

This was first published in October 2003

There are Comments. Add yours.

 
TIP: Want to include a code block in your comment? Use <pre> or <code> tags around the desired text. Ex: <code>insert code</code>

REGISTER or login:

Forgot Password?
By submitting you agree to receive email from TechTarget and its partners. If you reside outside of the United States, you consent to having your personal data transferred to and processed in the United States. Privacy
Sort by: OldestNewest

Forgot Password?

No problem! Submit your e-mail address below. We'll send you an email containing your password.

Your password has been sent to:

Disclaimer: Our Tips Exchange is a forum for you to share technical advice and expertise with your peers and to learn from other enterprise IT professionals. TechTarget provides the infrastructure to facilitate this sharing of information. However, we cannot guarantee the accuracy or validity of the material submitted. You agree that your use of the Ask The Expert services and your reliance on any questions, answers, information or other materials received through this Web site is at your own risk.