Steve Smith's Blog

Musings on Software and the Developer Community

Recursive FindControl

I've been asking for a recursive FindControl() method as a method off of System.Web.UI.Control for years but so far no luck.  You find yourself needing these frequently when you work with composite controls, like most of the Login family of controls introduced with ASP.NET 2.0.  In particular, LoginView, CreateUserWizard, and Login frequently require a technique like this to access their contents.

I posted a simple version a while back; Michael Palermo updated it, and Aaron Robson posted a nice generic version on IntrepidNoodle.com.  However, his site is often incredibly slow or non-responsive (perhaps he should switch to ORCSWeb), so while I'm happy to give him full credit, I'm going to mirror the content here so that I have access to it when I need it.

Of course, this can also be done as an Extension Method, which I'll show at the bottom of this post.

// Source: http://intrepidnoodle.com/articles/24.aspx
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;
    }
}

With this in place, you can easily access it from your Page by using a Base Page Class and adding these methods to it:

// Source: http://intrepidnoodle.com/articles/24.aspx
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);
    }

The next logical step is to just make this an extension method.  Brendan Enrick has already done this work for us, so I'll let you read his post to see how it's done.

Update 23 Sep 2008 in response to Howdy Doody's comment

Apologies - I left out how (and perhaps more importantly, why) you would use this.  We'll start with the why, which is that you have some some code that nests controls within controls.  For example, the Login control has a Password textbox inside of it (whether you template it or not).  If you want to get to this control to manipulate it, you can't just drop into Page_Load and say Password.Text = "foo".  Password is not defined at Page scope - it's only defined within the control tree of the Login control.  This can be a problem with any arbitrary control of your own if you use LoginView or MultiView controls as well.  For instance, this LoginView:

<asp:LoginView runat="server" id="LoginView1">
  <RoleGroups>
    <asp:RoleGroup Roles="Administrators">
      <ContentTemplate>
        <asp:Label runat="server" id="AdminMessageLabel" />
      </ContentTemplate>
    <asp:/RoleGroup>
</asp:LoginView>

If you want to set the contents of the AdminMessageLabel in Page_Load, you would not easily be able to do so.  However, you can get a reference to it like so, using a recursive FindControl<T> method, which in this case we'll assume is defined either in the page itself or in a common Base Page Class (recommended).

Label AdminMessageLabel = this.FindControl<Label>(LoginView1, "AdminMessageLabel");
if(AdminMessageLabel != null)
{
  AdminMessageLabel.Text = "You are an admin.";
}

Note that when the user is not an administrator, you won't have this control, so you need to check for null.  You also could pass in the Page as the starting position for the search for the control, but that would be much less efficient, and since we know it's in the LoginView1 control's collection of controls, we pass that for efficiency's sake.

Hope that helps - note that this last part was written via a web browser with no compiler access, so apologies in advance for any typos.

    kick it on DotNetKicks.com

Monday, 22 September 2008

Comments

 avatar

Andrei Rinea said on 23 Sep 2008 at 4:15 AM

Did you write to Microsoft about this? Did they say anything related to performance on why would they avoid including such a method?


ssmith avatar

ssmith said on 23 Sep 2008 at 9:40 AM

@andrei,

Pretty much everybody on the ASP.NET team I've discussed it with has been in favor of it, saying things like "yeah, we really should have that", etc.

It works fine for me in the situations where I'm using it. I'm sure there are ugly nested control situations where it might not do so well, but I suspect that is the exception not the rule and as usual there is some responsibility on the part of the developer to do the right thing.


 avatar

Howdy Doody said on 23 Sep 2008 at 7:19 PM

For those of us who are made of wood can you get it through your own thick head that you should always as a matter of practice include a few lines of code documenting how to actually use a method or a class? The ambiguity is often stymies a person otherwise eager to "get it."


 avatar

Thomas Hansen said on 04 Dec 2008 at 12:09 PM

This one is better, features a generic Predicate too in addition to one version returning an IEnumerable<T> etc... ra-ajax.org/.../jquery-ish-sele