aspalliance.com | RemASP Home | domains | authors.aspalliance.com | remas | ASP.NET | VFAQNET | TraverseControls

How to traverse/loop controls?

Search

 by Remas Wojciechowski

June 27th, 2002

This article shows how to traverse all subcontrols of a given root control.

At least once a week someone asks this question on some discussion list: How do I loop through all controls of a page. A common answer is: Simply use the Controls property, after all it contains a list of all subcontrols. Well, as often in life, the issue here is slightly more complicated than it seems at the first glance. Yes, the Controls property can be usueful for our task but it only returns direct children of a given control. However, these children can have children that can have children, etc., etc. Clearly, what we need here is a recursive function that traverses all controls in the control tree. In this article we will create a utilty class that returns an array with information about all subcontrols of a given root control.

First we will define a custom class ControlInfo that we will use to store basic data about our controls. For the purposes of this article we will define three properties in that class: ID, Type and TypeName. ID will hold (nomen omen) the id of a given control, Type will hold its type and TypeName will reflect the string representation of the type:

ControlInfo declaration
public class ControlInfo
{
  public ControlInfo(string id, Type type)
  {
    _ID = id;
    _Type = type;
  }
  public string ID
  {
    get
    {
      return _ID;
    }
    set
    {
      _ID = value;
    }
  }
  public Type Type
  {
    get
    {
      return _Type;
    }
    set
    {
      _Type = value;
    }
  }
  public string TypeName
  {
    get
    {
      return _Type.ToString();
    }
  }
  private string _ID;
  private Type _Type;
}

You're probably asking yourself why on Earth I'm creating an extra property for the TypeName while it only calls the ToString method of the Type object. This question will be answered when it comes to displaying the information about the controls.

Now that we have defined our custom class, we want to define a utilty class (ControlUtil) with a static method (TraverseControls) that returs an array of ControlInfo objects reflecting all subcontrols of a given root control. The TraverseControls method takes the root control as parameter and uses its Controls property that returns all direct children of the root. Since these children can possibly have child controls themselves, TraverseControls invokes itself as long as it encounters a childless control.

ControlUtil class definition
public class ControlUtil
{
  public static ControlInfo[] TraverseControls(System.Web.UI.Control root)
  {
    if (root.Controls.Count > 0)
    {
      ArrayList al = new ArrayList();
      al.Add(new ControlInfo(root.ID, root.GetType()));
      foreach (System.Web.UI.Control myCtrl in root.Controls)
      {
        al.AddRange(TraverseControls(myCtrl));
      }
      return (ControlInfo[]) al.ToArray(typeof(ControlInfo));
    }
    else
    {
      return new ControlInfo[] {new ControlInfo(root.ID, root.GetType())};
    }
  }
}

Let's now have a quick look at how we can utilize our class. We want to display a table of all controls with their id and type. To make it least complicated, we're going to use a DataGrid to that end. A nice feature of the DataGrid class is that it accepts arrays for its DataSource property. In its simplest form (no data-binding expressions, etc.) it will create a column for each value type. Yes, now you know why I created that extra property TypeName.

Using the ControlUtil class
DataGrid dg = new DataGrid();
ControlInfo[] myInfo = ControlUtil.TraverseControls(Page);
dg.DataSource = myInfo;
dg.DataBind();
Page.Controls.Add(dg);

In this example we have called the TraverseControls method for the Page. Of course, we could have used it for any control on our page.

What follows is a sample ASPX form and the result that the above code yields.

Sample ASPX form
<HTML>
  <HEAD>
    <title>Sample ASPX form</title>
  </HEAD>
  <body>
    <form id="test" method="post" runat="server">
      <asp:Panel id="Panel1" runat="server">Panel</asp:Panel>
      <asp:Table id="Table1" runat="server">
        <asp:TableRow>
          <asp:TableCell>
          <asp:TextBox id="TextBox1" runat="server"></asp:TextBox>
          </asp:TableCell>
          <asp:TableCell><asp:Label id="Label1" runat="server">Label</asp:Label>
          </asp:TableCell>
        </asp:TableRow>
      </asp:Table>
      <asp:Button id="Button1" runat="server" Text="Button"></asp:Button>
    </form>
  </body>
</HTML>
Result of using the ControlUtil class
IDTypeName
 ASP.test_aspx
 System.Web.UI.LiteralControl
testSystem.Web.UI.HtmlControls.HtmlForm
 System.Web.UI.LiteralControl
Panel1System.Web.UI.WebControls.Panel
 System.Web.UI.LiteralControl
 System.Web.UI.LiteralControl
Table1System.Web.UI.WebControls.Table
 System.Web.UI.WebControls.TableRow
 System.Web.UI.WebControls.TableCell
TextBox1System.Web.UI.WebControls.TextBox
 System.Web.UI.WebControls.TableCell
Label1System.Web.UI.WebControls.Label
 System.Web.UI.LiteralControl
Button1System.Web.UI.WebControls.Button
 System.Web.UI.LiteralControl
 System.Web.UI.LiteralControl

Depending on your needs you can extend both the ControlInfo and the ControlUtil class. Here's, for instance, an overload of the TraverseControls method that lets you supress certain types from being returned. You can provide an array of Type objects and no controls of these types will be added to the resulting array. Note, that in this implementation child controls of a supressed type are not ignored.

Extending the ControlUtil class
public static ControlInfo[] TraverseControls(Control root, Type[] filter)
{
  bool Supress;
  ControlInfo[] tmpInfo;
  if (root.Controls.Count > 0)
  {
    ArrayList al = new ArrayList();
    Supress = false;
    for (int i = 0; i < filter.Length; i++)
    {
      if (Type.Equals(filter[i], root.GetType()))
      {
        Supress = true;
        break;
      }
    }
    if (!Supress) al.Add(new ControlInfo(root.ID, root.GetType()));
    foreach (Control myCtrl in root.Controls)
    {
      tmpInfo = TraverseControls(myCtrl, filter);
      if (tmpInfo != null) al.AddRange(tmpInfo);
    }
    return (ControlInfo[]) al.ToArray(typeof(ControlInfo));
  }
  else
  {
    Supress = false;
    for (int i = 0; i < filter.Length; i++)
    {
      if (Type.Equals(filter[i], root.GetType()))
      {
        Supress = true;
        break;
      }
    }
    if (!Supress) return new ControlInfo[] {new ControlInfo(root.ID, root.GetType())};
    else return null;
  }
}

Let's now have a look at the result of our sample code when we modify the method call to invoke the new overload. Here we're trying to filter out all controls of type System.Web.UI.LiteralControl.

Using the ControlUtil class with a type filter
DataGrid dg = new DataGrid();
ControlInfo[] myInfo =
  ControlUtil.TraverseControls(Page, new Type[] {typeof(LiteralControl)});
dg.DataSource = myInfo;
dg.DataBind();
Page.Controls.Add(dg);
Result of using the ControlUtil class with a type filter
IDTypeName
 ASP.test_aspx
testSystem.Web.UI.HtmlControls.HtmlForm
Panel1System.Web.UI.WebControls.Panel
Table1System.Web.UI.WebControls.Table
 System.Web.UI.WebControls.TableRow
 System.Web.UI.WebControls.TableCell
TextBox1System.Web.UI.WebControls.TextBox
 System.Web.UI.WebControls.TableCell
Label1System.Web.UI.WebControls.Label
Button1System.Web.UI.WebControls.Button

I hope you'll find this little utility class useful.