Maintain ASP.NET Tree Position on Postbacks

The TreeView in ASP.NET 2.0 is a great control, but it seems to have one shortcoming: it isn't good at keeping track of the last selected position between postbacks. If you scroll down to the fiftieth item and expand a parent node, you have to start over at the top when the page returns.

In the previous version of .NET, you might have considered using the SmartNavigation feature instead. SmartNavigation is a property of a Web page. It is a Boolean that you can set in the Page directive or code behind. A Page directive with this attribute set to true may look like this:

<%@ Page Language="VB" AutoEventWireup="false"
    CodeFile="Default.aspx.vb"
    Inherits="_Default" SmartNavigation="true" %>

However, as many have noted, SmartNavigation has issues. In fact, Microsoft has deprecated this feature in ASP.NET 2.0 and added the MaintainScrollbackPositionOnPostback feature (that's a mouthful). Unfortunately, I haven't had a lot of luck with that one either (I'll explain why shortly).

This article shows where the SmartNavigation and MaintainScrollbackPositionOnPostback features fall short in maintaining tree position on postback and provides a JavaScript solution that seems to do the trick, even for complicated Web pages.

Bye-Bye SmartNavigation, Hello MaintainScrollbackPositionOnPostback

SmartNavigation is supposed to eliminate page flashing due to navigation. It does this by approximating IFrames and rendering only what has changed. SmartNavigation also is supposed to persist the scroll position, persist focused elements, and retain the last page in the browser's history. Unfortunately, even Microsoft knows SmartNavigation is broken. Check the MSDN help documentation and you will see that SmartNavigation is considered obsolete. Check Google and you will find a plethora of threads and discussions about the problems SmartNavigation has.

Next.

ASP.NET 2.0 introduces MaintainScrollbackPositionOnPostback. Like SmartNavigation, you can also set it to true or false in code or in the Page property:

<%@ Page Language="VB" AutoEventWireup="false"
    CodeFile="Default.aspx.vb"
    Inherits="_Default" MaintainScrollPositionOnPostback="true" %>

Quite literally, this property/attribute is supposed to maintain scroll position. Unfortunately it seems to do it only on the page itself. Hence, if you are in a node in a TreeView that is nested in a panel or UserControl, this feature will return you to that control but not a node in that control.

In short, MaintainScrollbackPositionOnPostback is a good feature if you have a page that scrolls. However, if your page is fixed—like an application might be—then it isn't going to help. If the only thing on the page is a long list that scrolls—like a TreeView—then this is the feature for you.

Maintaining Scroll Position within a TreeView

Recently, I developed a Web application that was sort of "Windowsy." That is, each Web page took up the whole screen but did not scroll. Controls within the pages scrolled like a TreeView for navigation and like a grid, but not the page itself. This application had sortable, hierarchical data listed in an n-nested TreeView. The problem was that sometimes the volume of data was extensive, necessitating annoying scrolling and re-scrolling as one navigated through the tree. Determined to resolve this problem, I devised the following solution.

First, the TreeView knows which node is selected because SelectedNode is a property of the TreeView control. This information has to be stored somewhere, and what is selected ultimately is rendered as HTML. If I know the ID of the selected HTML control, then I should be able to scroll and focus it. Sure enough, if you look at the source HTML of a page with a TreeView on it, you will find the declaration of a hidden <INPUT> tag—think text box—whose ID is approximately TreeViewx_SelectedNode:

<input type="hidden" name="TreeView1_SelectedNode"
       id="TreeView1_SelectedNode"
       value="TreeView1t54" />

With this knowledge, the idea is that a hidden input control is basically a textbox and all you need to do is figure out what the contents are. A TreeView is rendered as an HTML table—that's all ASP.NET controls are, little HTML code generators—and the value field is the client identifier of a table cell, a <TD> tag representing the node. So find the ID of the cell tag, scroll it into view, and you are done.

[Note to ASP.NET team: This should be a property of the TreeView. Thank you.]

To demonstrate, I wrote some code to generate a really full TreeView. The Page_Load event injects some JavaScript to find and focus the table cell (see Listing 1) and the <body> tag's onload event calls this injected function.

Listing 1: The Code Behind That Fills a Treeview and Injects the JavaScript

Imports System.Collections.Generic


Partial Class _Default
   Inherits System.Web.UI.Page

   Protected Sub Page_Load(ByVal sender As Object, _
      ByVal e As System.EventArgs) Handles Me.Load

      InjectLoadEvent()

      If (IsPostBack) Then Return

      TreeView1.Nodes.Clear()

      Dim chicken As New TreeNode("Chicken")
      TreeView1.Nodes.Add(chicken)
      Dim beef As New TreeNode("Beef")
      TreeView1.Nodes.Add(beef)
      Dim pork As New TreeNode("Pork")
      TreeView1.Nodes.Add(pork)
      Dim fish As New TreeNode("Fish")
      TreeView1.Nodes.Add(fish)


      chicken.ChildNodes.Add(New TreeNode("Crepes Florentine with _
                                           Buffalo Chicken"))
      fish.ChildNodes.Add(New TreeNode("Linguine with White Clam _
                                        Sauce"))
      pork.ChildNodes.Add(New TreeNode("Pork Loin with Peanut and _
                                        Madarin Orange Sauce"))
      beef.ChildNodes.Add(New TreeNode("Standing Rib Roast with _
                                        Fennel and Blue Cheese _
                                        Potatoes"))


      ' We need a bunch of stuff here so we will add some stubs
      Dim I As Integer
      For I = 1 To 50
         chicken.ChildNodes.Add(New TreeNode("Placeholder " + _
                                             I.ToString()))
         fish.ChildNodes.Add(New TreeNode("Placeholder " + _
                                          I.ToString()))
         pork.ChildNodes.Add(New TreeNode("Placeholder " + _
                                          I.ToString()))
         beef.ChildNodes.Add(New TreeNode("Placeholder " + _
                                          I.ToString()))
      Next

      TreeView1.CollapseAll()
   End Sub

   Public Sub InjectLoadEvent()
      Dim script As String = _
        "function LoadEvent()" + _
        "{{" + _
        " try" + _
        " {{" + _
        "   var elem = document.getElementById('{0}_SelectedNode');" + _
        "   if(elem != null )" + _
        "   {{" + _
        "     var node = document.getElementById(elem.value);" + _
        "     if(node != null)" + _
        "     {{" + _
        "       node.scrollIntoView(true);" + _
        "       {1}.scrollLeft = 0;" + _
        "     }}" + _
        "   }}" + _
        " }}" + _
        " catch(oException)" + _
        " {{}}" + _
        "}}"

      Page.ClientScript.RegisterClientScriptBlock(Me.GetType(), _
        "LoadEvent", _
        String.Format(script, TreeView1.ClientID, Panel1.ClientID), _
        True)
   End Sub
End Class

Listing 2 shows the complete page.

Listing 2: The ASP Code That Defines the Appearance of this Relatively Simple Page

<%@ Page Language="VB" AutoEventWireup="false"
         CodeFile="Default.aspx.vb" Inherits="_Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
   <title>Focus Tree Node on Postback</title>
</head>
<body onload="LoadEvent()">
   <form id="form1" runat="server">
   <div>
      <asp:Panel ID="Panel1" runat="server" Height="200px"
                 Width="200px" ScrollBars="Both">
      <asp:TreeView ID="TreeView1" runat="server">
         <SelectedNodeStyle BackColor="#8080FF" />
      </asp:TreeView>
      </asp:Panel>
   </div>
   </form>
</body>
</html>

Figure 1 presents an example of what the simple page looks like.

Figure 1: A TreeView Nested in a Scrollable Panel Control (in IE7)

Finally, Listing 3 shows the resultant JavaScript, which is the interesting part.

Listing 3: The Injected JavaScript

<script>
   function LoadEvent()
   {
      try
      {
         var elem = document.getElementById('TreeView1_SelectedNode');
         if(elem != null )
         {
            var node = document.getElementById(elem.value);
            if(node != null)
            {
               node.scrollIntoView(true);
               Panel1.scrollLeft = 0;
            }
         }
      }
      catch(oException)
      {}
   }// -->
</script>

The JavaScript function LoadEvent finds the hidden input field. I injected all of the code expect the ClientID of the TreeView, which in this case is TreeView1 but in deeply nested pages can have an ungodly long name. When I get that control, I use its value property to find the table cell, and with that control I invoke scrollIntoView. The scrollLeft property keeps the Panel scrolled to the left. And it works!

A Missing Function Is Found

Microsoft doesn't have time to do everything. So we're left with some fun and interesting problems—large and small—to solve. I hope you find this technique useful. If it helps you out, tell your friends where you got it (developer.com) and who wrote it (Me), and tune in to the same Bat-time, same Bat-channel for more useful techniques.

About the Author

Paul Kimmel is the VB Today columnist for www.codeguru.com and has written several books on object-oriented programming, including Visual Basic .NET Power Coding (Addison-Wesley) and UML Demystified (McGraw-Hill/Osborne). He is the president and co-founder of the Greater Lansing Area Users Group for .NET (www.glugnet.org) and a Microsoft Visual Developer MVP.

Copyright © 2006 by Paul T. Kimmel. All Rights Reserved.



Comments

  • It is necessary to select the node...

    Posted by dalerimi on 02/13/2014 07:47am

    This solution works only setting the selected node at the level of programming because the selection of the node does not happen to the expansion of the same.

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

Top White Papers and Webcasts

  • Live Event Date: May 7, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT This eSeminar will explore three popular games engines and how they empower developers to create exciting, graphically rich, and high-performance games for Android® on Intel® Architecture. Join us for a deep dive as experts describe the features, tools, and common challenges using Marmalade, App Game Kit, and Havok game engines, as well as a discussion of the pros and cons of each engine and how they fit into your development …

  • Instead of only managing projects organizations do need to manage value! "Doing the right things" and "doing things right" are the essential ingredients for successful software and systems delivery. Unfortunately, with distributed delivery spanning multiple disciplines, geographies and time zones, many organizations struggle with teams working in silos, broken lines of communication, lack of collaboration, inadequate traceability, and poor project visibility. This often results in organizations "doing the …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds