Recursively searching controls in ASP.NET web forms using generics, lambda expressions and extension methods!

Extension methods were introduced with the .NET 3.5 framework as a mechanism to add methods to extend existing types without modifying the original assembly. This is how the Linq methods were implemented to enable some very powerfull predicate function based operations to be performed over all existing collection types.

Searching for web controls on a page is one of those tasks that seems to come up for all kinds of reason while programming using web forms. I was reminded of this problem recently:

I'm personally favoring the MVC framework now, however, while at work the other day one of my collegues was working through an old web forms project of mine where a variable number of checkbox controls were being rendered in two separate lists on the page. Them, on post-back he needed to get a list of the checkboxes that were now checked.

Lambda expressions combined with recursive calls are a very powerful way of seaching through a pages controls. Originally I just used a simple funciton defined in the code-behind, however a much cleaner and reusable method would be to define the functions on the control class its self.

That's where extension methods come in. The code below shows a nice simple example of a couple of useful control search functions which then appear inside any object inheriting from System.Web.UI.Control.

To define extension methods on an exising class in C# you would do the following:

Firstly create a public static class with whatever name you like in whatever namespace you like (I've defined mine in DanielBradley.WebControlExtensions.ControlExtensions).

Secondly, define a public static (shared) function where you specify the first parameter with the keyword "this" and it's type as the type of the class you want to extend.

Finally, define everything else exactly how you would normally, with the correct return type, your other parameters and you simply look at your first parameter as the current object the functions is running in.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.UI;
 
namespace DanielBradley.WebControlExtensions
{
    public static class ControlExtensions
    {
        public static Control FirstOrDefault<TSource>(this Control ctrl, Func<TSource, bool> predicate) where TSource : Control
        {
            Type targetType = typeof(TSource);
            foreach (Control c in ctrl.Controls)
            {
                if (c.GetType() == targetType && predicate((TSource)c))
                {
                    return c;
                }
                Control recMatch = c.FirstOrDefault<TSource>(predicate);
                if (recMatch != null)
                {
                    return recMatch;
                }
            }
            return null;
        }
 
        public static IEnumerable<TSource> FindRecursive<TSource>(this Control ctrl, Func<TSource, bool> predicate) where TSource : Control
        {
            if (ctrl == null || ctrl.Controls.Count == 0)
                return new List<TSource>();
 
            return ctrl.Controls.OfType<TSource>().Where(predicate).Union(
                ctrl.Controls.Cast<Control>().SelectMany(c => c.FindRecursive<TSource>(predicate)));
        }
 
        public static IEnumerable<TSource> FindRecursive<TSource>(this Control ctrl, Func<TSource, bool> predicate, int depthLimit) where TSource : Control
        {
            if (ctrl == null || ctrl.Controls.Count == 0)
                return new List<TSource>();
 
            if (depthLimit == 0)
            {
                return ctrl.Controls.OfType<TSource>().Where(predicate);
            }
            else
            {
                return ctrl.Controls.OfType<TSource>().Where(predicate).Union(
                    ctrl.Controls.Cast<Control>().SelectMany(c => c.FindRecursive<TSource>(predicate, depthLimit - 1)));
            }
        }
    }
}
 

Extension methods in C# are implemented as a specific language feature, however, you can also implement extension methods in VB.NET through use of attributes:

Firstly import System.Runtime.CompilerServices.

Secondly define your (public) function, add the Extension attribute and define a first parameter as the type you want to add the function to (into which the object your function is running on is passed in to).

Here's the equivilant VB code (actually re-written not auto-converted :)

Imports System.Runtime.CompilerServices
Imports System.Web.UI
 
Public Module ControlExtensions
 
    <Extension()> _
    Public Function FirstOrDefault(Of TSource As Control)(ByVal ctrl As Control, ByVal predicate As Func(Of TSource, Boolean))
        Dim targetType = GetType(TSource)
        For Each c As Control In ctrl.Controls
            If c.GetType.Equals(targetType) AndAlso predicate(c) Then
                Return c
            End If
            Dim recMatch = c.FirstOrDefault(predicate)
            If recMatch IsNot Nothing Then
                Return recMatch
            End If
        Next
        Return Nothing
    End Function

    <Extension()> _
    Public Function FindRecursive(Of TSource As Control)(ByVal ctrl As Control, ByVal predicate As Func(Of TSource, Boolean))
        If ctrl Is Nothing OrElse ctrl.Controls.Count = 0 Then Return New List(Of TSource)
 
        Return ctrl.Controls.OfType(Of TSource).Where(predicate).Union( _
        ctrl.Controls.Cast(Of Control).SelectMany(Of TSource)(Function(c) c.FindRecursive(predicate)))
    End Function
 
    <Extension()> _
    Public Function FindRecursive(Of TSource As Control)(ByVal ctrl As Control, ByVal predicate As Func(Of TSource, Boolean), ByVal depthLimit As Integer)
        If ctrl Is Nothing OrElse ctrl.Controls.Count = 0 Then Return New List(Of TSource)
 
        If depthLimit = 0 Then
            Return ctrl.Controls.OfType(Of TSource).Where(predicate)
        Else
            Return ctrl.Controls.OfType(Of TSource).Where(predicate).Union( _
            ctrl.Controls.Cast(Of Control).SelectMany(Of TSource)(Function(c) c.FindRecursive(predicate, depthLimit - 1)))
        End If
    End Function
 
End Module

Having written these examples there's loads of ideas of useful stuff that's springing to mind that you could extend from here:

Anyway, there's the post, hope this is useful - might perhaps pad this out a little then move it into a project on github or something if it's potentially useful to people.

Daniel