Created On:
23/04/2007
Author:
Aaron Robson
Tags:
c#, asp.net

Recursive FindControl Simplified - Update

In a previous post, I refactored some code to produce a simplified recursive FindControl method. While the refactoring was faithful to the original functionality, I failed to investigate whether the functionality was actually correct. It turns out it wasn't :)

The original version of the code didn't drill down the hierarchy of a control which was of the requested generic type but which wasn't the correct Id we were looking for.

Here is a new class which should work:

public static class ControlFinder
{
/// <summary>
/// Similar to Control.FindControl, but recurses through child controls.
/// </summary>
public static T FindControl<T>(Control startingControl, string id) where T : Control
{
T found = startingControl.FindControl(id) as T;

if (found == null)
{
found = FindChildControl<T>(startingControl, id);
}

return found;
}

/// <summary>
/// Similar to Control.FindControl, but recurses through child controls.

/// Assumes that startingControl is NOT the control you are searching for.
/// </summary>
public static T FindChildControl<T>(Control startingControl, string id) where T : Control
{
T found = null;

foreach (Control activeControl in startingControl.Controls)
{
found = activeControl as T;

if (found == null || (string.Compare(id, found.ID, true) != 0))
{
found = FindChildControl<T>(activeControl, id);
}

if (found != null)
{
break;
}
}

return found;
}
}

In your base page, I suggest having these methods:

    public T FindControl<T>(string id) where T : Control
{
// We know the control we're searching for isn't the Page itself,
// so we use the more performant FindChildControl to search.
return FindChildControl<T>(Page, id);
}

public T FindControl<T>(Control startingControl, string id) where T : Control
{
return ControlFinder.FindControl<T>(startingControl, id);
}

public T FindChildControl<T>(Control startingControl, string id) where T : Control
{
return ControlFinder.FindChildControl<T>(startingControl, id);
}

There is a potential issue which I *think* could occur with Composite Controls which are not implemented according to guidelines or inherited from CompositeControl. This is related to the fact that EnsureChildControls() may not be called, and without a call to Control.FindControl, we've no way of forcing that to occur.

The result would most likely be null being returned when you might expect to have found a control. I suspect this issue could affect other dynamically created controls such as (the contents of) databound controls. This would only occur if you were searching for the id of one of the child controls contained by the composite / databound control.

If anyone has (or does) hit this issue, let me know, and if you work on the asp.net team or have had the time to 'reflector' through this scenario, I'd be grateful for clarification.


Comments

Grant Maw -
Thanks for this Aaron. I have a situation where I need programmatic access to controls on nested master pages. Your solution handles my situation perfectly.
dotnetnoobie -
I made a vb version

Partial Class Default4 Inherits System.Web.UI.Page
    Public Overloads Function FindControl(Of T As Control)(ByVal id As String) As T
        Return FindChildControl(Of T)(Page, id)
    End Function

    Public Overloads Function FindControl(Of T As Control)(ByVal startingControl As Control, ByVal id As String) As T
        Return ControlFinder.FindControl(Of T)(startingControl, id)
    End Function

    Public Function FindChildControl(Of T As Control)(ByVal startingControl As Control, ByVal id As String) As T
        Return ControlFinder.FindChildControl(Of T)(startingControl, id)
    End Function

    Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
        Me.FindControl(Of RadioButton)(Page, "RadioButton1").Checked = True
    End Sub

End Class

Public NotInheritable Class ControlFinder
    Private Sub New()
        MyBase.New()
    End Sub
   
    Public Shared Function FindControl(Of T As Control)(ByVal startingControl As Control, ByVal id As String) As T
        Dim found As T = TryCast(startingControl.FindControl(id), T)
   
        If found Is Nothing Then
            found = FindChildControl(Of T)(startingControl, id)
        End If

        Return found
    End Function

    Public Shared Function FindChildControl(Of T As Control)(ByVal startingControl As Control, ByVal id As String) As T
        Dim found As T = Nothing
       
        For Each activeControl As Control In startingControl.Controls
            found = TryCast(activeControl, T)
   
            If found Is Nothing OrElse (String.Compare(id, found.ID, True) <> 0) Then
                found = FindChildControl(Of T)(activeControl, id)
            End If

            If found IsNot Nothing Then
                Exit For
            End If
        Next

        Return found
    End Function

End Class
Aaron -
Hi Grant,

Great, I'm pleased its working out - I haven't actually got round to using it myself yet - I've still got some older code doing the job. Let me know if any issues come up.

DotnetNoobie - thx for the vb version, I formatted your comment a bit.

Terry Olsen -
Thanks! I needed to recurse through several child windows and tabcontrols to find the texteditor control in each one. I was able to adapt this code for my windows forms app simply by changing the startingControl.FindControl to startingControl.Controls.
JoshK -
Why not just write the extension method for IEnumerable<Control>? Would make the code even simpler :-) Unless I am overlooking something. (I do realise the code is of age).
Jorge Salinas -
Excelent Code, great help, thanks man. You Rock!
Allan -
Thanks :) Good job!
Sergio Rossetti -
Thanks for this methods. They really helped me. I was looking for something like this. Regards.