Recursive Page.FindControl

I'm currently writing my first ASP.NET 2.0 website. VS.NET 2005 is worlds better than VS.NET 2003, but I was mildly surprised to find that Microsoft still hasn't added a recursive overload for Page.FindControl. So, courtesy of Oddur Magnusson, here it is:


This is a companion discussion topic for the original blog entry at: http://www.codinghorror.com/blog/2005/06/recursive-pagefindcontrol.html

FWIW, it’s not a recommended practice to rely on the IDs automatically generated by a naming container …

Thank you for this article. The recursive functiion works fine. I often got angry about the function FindControl() which did not work in some situations.

I changed the code a bit to return all controls matching the given ID. W3 states that IDs should be unique, but non-unique IDs have never given me any trouble. In any case, the code can easily be changed to match by some other attribute.

Function FindControls( root As Control, id As String ) As ArrayList

	Dim control_list As New ArrayList
	
	If root.ID = id Then control_list.Add( root )
	
	For Each current_control As Control in root.Controls
		Dim control_list_subset = FindControls( current_control, id )
		control_list.AddRange( control_list_subset )
	Next
	
	FindControls = control_list
End Function

It appears the RTM version of ASP.NET 2.0 does support recursive FindControl behavior.

It may not be recommended to rely on auto generated IDs but after looking at the find control method it actually uses the colon count to determine the path offset and not the ID names. So as long as the control depth does not change and the control to be found is not using an autogenerated ID number it should be ok to use the : method to find a control.

It is not preferable to use the recursive find only because it is possible for two controls to exist with the same ID’s in different control hierarchies, so what the above method is doing is only finding the first instance with the matching id and not all other controls that may also contain the id.

Here’s his exact function translated into VB.NET. Save yourself 30 seconds :slight_smile:

Private Function FindControlRecursive(ByVal root As Control, ByVal id As String) As Control
    If root.ID = id Then
        Return root
    End If
    Dim c As Control
    For Each c In root.Controls
        Dim t As Control = FindControlRecursive(c, id)
        If Not t Is Nothing Then
            Return t
        End If
    Next
    Return Nothing
End Function

Would this work if the same control was on the page twice? Which instance would it return?

Bad Karma, you cannot have two controls on the page with the same ID.

This, the findcontrol(String control_ID) function works by returning the one and only control with the specified id (or null if it was not found).

I was sent to this method by Tim Rayburn and am ever greatful for that. I’ve been grabbing controls out of repeaters a very nasty way so far and after showing it to Tim he pointed me to this blog. MANY thanks for the code, it’s so much more reliable and easier to use.

Keep up the great blogging (btw, I’ve been reading for about a year, but this post goes back beyond my time, so thanks!)
–j

I just want to say thank you for posting this code!! It has saved me further headaches!

Thank you!!
John

Just wondering about BadKarmas comments:

“Would this work if the same control was on the page twice? Which instance would it return?”

And the annonymous response:

"Bad Karma, you cannot have two controls on the page with the same ID.

This, the findcontrol(String control_ID) function works by returning the one and only control with the specified id (or null if it was not found)."

I am not sure about this but its true that no controls directly added within a page can have the same ID. Though, controls inside in web user controls can have the same ID as one in the parent page. The resultin ClientID’s will be different but that isn’t what this recursive function is comparing. So I do wonder if the resulting control returned will always be correct?

Chris I think you’re right. If some Control in the Page is a INamingContainer (CustomControls are, for example), it is indeed allowed to hold a Control which ID is can be found elsewhere in the Page (or for that matter in other naming containers). For instance, with a GridView, each GridViewRow is a naming container so that with the repeating pattern, controls with same IDs can be created, but are still unique per row.

This recursive FindControl will return the first found occurence only. That may be the reason why there is no recursive FindControl within the framework.

Maybe and equivalent method, but that returns a ListControl or such? Then you may identify each control by its ClientID property.

Something to find the control on the page even if it is in a INamingContainer would be very sweet. May have to use control and clientID to find it.

It seems that my comment got striped from the angel brackets so here it is again (html encoded)

use the code like this: CheckBox box = this.FindControlRecursiveCheckBox(“chkEmail”);

public static T FindControlRecursiveT(this Control parentControl, string id) where T : Control
{
T ctrl = default(T);

if ((parentControl is T) (parentControl.ID == id))
return (T)parentControl;

foreach (Control c in parentControl.Controls)
{
ctrl = c.FindControlRecursiveT(id);

if (ctrl != null)
break;
}
return ctrl;
}

Jeff thanks for your code, it really helped me out.

I went and created a generic version of it so you dont have to cast the control, an additional benefit of generics is that we now have the type of control you expect to find which means we can first compare the type and then the id which saves us a LOT of processing time since type comparison is much faster then string comparison, I also made it a extension method which means we can invoke it from any control. (like: CheckBox box = this.FindControlRecursiveCheckBox(“chkEmail”);

Thanks
-James

Here is the code:
public static T FindControlRecursiveT(this Control parentControl, string id) where T : Control
{
T ctrl = default(T);

if ((parentControl is T)  (parentControl.ID == id))
    return (T)parentControl;

foreach (Control c in parentControl.Controls)
{
    ctrl = c.FindControlRecursiveT(id);

    if (ctrl != null)
        break;
}
return ctrl;

}

Our lead developer found the sample useful also. He modified it to include a list of same named controls per Olivier Hoareau’ comment as follows:

Public Shared Sub c_FindControlListRecursive(ByVal root As Control, _
ByVal id As String, _
ByRef loc As List(Of Control))

’ Found on an internet blog: http://www.codinghorror.com/blog/archives/000307.html
’ Programming and Human Factors
’ by Jeff Atwood

’ Modified by HBP: To return a List (of Control) instead of just the first matching control.

’ Because: It’s possible to have more than one control with the same ID if some are
’ buried within other controls. If more than one control is returned, it’s
’ left as an exercise for the user to determine which one is actually desired.
’ Example:

’ ’ Initialize the keyboard heading:
’ Dim loc As New List(Of Control)
’ c_FindControlListRecursive(Me.Page, “lblAccKeyBoardFullHeading”, loc)

    '   ' Since, in the case of multiple controls found, I have no clue as to which one I actually want, I'll set them all:
    '   For Each control In loc
    '
    '       Dim lblAccKeyBoardFullHeading As Label = CType(control, Label)
    '       lblAccKeyBoardFullHeading.Text = sLabelHeading   ' For example, "Type the first few letters of your last name."
    '
    '   Next control
    '

     If root.ID = id Then
        loc.Add(root)
    End If

    Dim c As Control

    For Each c In root.Controls
        Dim tloc As New List(Of Control)
        c_FindControlListRecursive(c, id, tloc)
        For Each cntrl As Control In tloc
            loc.Add(cntrl)
        Next cntrl
    Next c

End Sub 

I haven’t looked further to determine how to uniquely identify the controls from there but maybe it will help someone!

Regards,

Mick

private Control FindControlRecursive(Control root, string id)
{
if (root.ID == id)
{
return root;
}

foreach (Control c in root.Controls) 
{ 
    Control t = FindControlRecursive(c, id); 
    if (t != null) 
    { 
        return t; 
    } 
} 

return null; 

}
Thanks for u all,this function works to find the Control. But i need the type of the the control and handle according to the control.

TAKE CARE!!!

I’ve just finished fixing a bug, the cause of which I traced to a helper function I had written called, funnily enough:

_findControlRecursive(string controlID, Control root)

The implementation is functionally identical to that given above.

I was using it to find an ObjecDataSource using the Page control as the root. Somehow (I haven’t worked out exact mechanism) using the Page as the result control fudged up the event binding of a grid so it’s update event wouldn’t fire.

Using reflector, I looked at how a GridView finds a data source from the DataSourceID property… it doesn’t do it recursively. Instead it cycles up the NamingContainers until a control with the given id is found using the regular FindControl.

You can see this using Reflector in DataBoundControlHelper.FindControl()

Reimplementing my own helper in a similar fashion to this non-recursive method works flawlessly so far.

In other words, at the minimum, DON’T DO THIS:

FindControlRecursive(Page, MyControlID)

…because it interferes with event binding in some way.

Hope that saves someone some time (took me ages to sort this damn bug!)

Cheers,
Sam

p.s. Typo above…

Replace
using the Page as the result control
with
using the Page as the root control