Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Very Efficient Generic Recursive Iterator using Yield Return

0.00/5 (No votes)
1 Jun 2007 1  
This article demonstrates techniques on how to optimise the common programming task of recursively iterating through a container control using generics and yield return.

Introduction

An often performed exercise by developers is to recursively iterate through all controls within a control container (like a Form or Panel) as well as all their sub-containers.

The code I list here is a simple yet very efficient generic way of doing this. It is by all means no world shocking revolutionary break-through but it is nice in its sheer simplicity!!

The Task

Say an invalid command has taken place and you wish to set all TextBoxes on your form to show "n/a". So you will have to recursively find all TextBoxes in this Form's container and all its other sub-containers that it may contain.

An Inefficient Though Often Used Approach

An often used method would be to create a separate routine that brings back a list of TextBox controls that can then be set to "n/a" within the calling routine. Something like this...

public static List<Control> GetControlsOfType(Control ctrMain)
{
    List<Control> list = new List<Control>();

    foreach (Control c in ctrMain.Controls)
    {
        if (c.GetType().Equals(typeof(TextBox)))
            list.Add(c);

        if (c.Controls.Count > 0)
            list.AddRange(GetControlsOfType(c));
    }

    return list;
}

... used like this:

private void button1_Click(object sender, EventArgs e)
{
    foreach (Control c in Utils.GetControlsOfType(this))
        c.Text = "n/a";
}

This technique works but has some ugly disadvantages:

  • What if the need arises to find all Buttons instead of TextBoxes within your Form? You will have to duplicate your code or put a switch in it to distinguish between control Types.
  • You are creating a new List that is probably used only once after which it is earmarked for Garbage Collection. If you do this often and potentially with many more items, then this could have a real impact on your memory footprint and code efficiency. Remember that you are after all creating and populating a List with ALL the items only to set a Text property after which you no longer need this long list and dispose of it!!

The Generic IEnumerable Approach

Now have a look at the code below. By using Generics, we tackle the first issue of being bound to a specific Type. We are now forced to specify the Type we wish to search for when calling the method by specifying <T>.

Then by using the yield return statement and returning the generic IEnumerable<T> instead of a fully populated temporary List, we are able to dramatically reduce our memory footprint and make the code much more efficient!!

public delegate bool delTypeCheck(Type type);

/// <summary>
/// Examines the full content of this control's Controls 
/// collection and returns the enumerator instead of a generic list.
/// </summary>
/// <typeparam name="T">The Type we are looking for - 
/// we will also treat any derived types as a match!</typeparam>
/// <param name="ctrMain">The control to examine.</param>
/// <param name="IsExemptCheck">A delegate allowing us to specify 
/// which controls to exempt from examining.</param>
/// <returns>The actual enumerator allowing us to NOT have 
/// to create a helper intermediate list.</returns>
public static IEnumerable<T> GetControlsOfType<T>
	(Control ctrMain, delTypeCheck IsExemptCheck) where T : class
{
    // Determine the Type we need to look out for
    Type searchType = typeof(T);

    foreach (Control c in ctrMain.Controls)
    {
        // If user wants to exclude certain types then a call-back has been given
        // So call it with the current control-type and check if it is exempt.
        if (IsExemptCheck != null && IsExemptCheck(c.GetType()))
            continue;   // the type of c is exempt so continue.

        // If a match is found then yield this item back directly    
        if (c is T) yield return (c as T);
            
        // if you want to search for specific Types only (and NOT derived types) then
        // uncomment the following lines (and comment out the above statement).
        //if (c.GetType().Equals(searchType))
        //    yield return (c as T);

        // If the control hosts other controls then recursively call this function again.
        if (c.Controls.Count > 0)
            foreach (T t in GetControlsOfType<T>(c, IsExemptCheck))
                yield return t;
    }
}

This is used like this.

If we want to find all controls that derive from TextBoxBase (which includes TextBoxes as well as RichTextBox) and set the Text property of all these to "n/a", then implement something like the code below:

foreach (Control c in Utils.GetControlsOfType<TextBoxBase>(this, null))
{
     c.Text = "n/a";
}

In addition, exclude certain Types from examining.

Say you want to exclude searching through some Types of containers (in this case Panel), then you can supply an anonymous delegate to exclude these (see code below). The 'IsExemptCheck' delegate hands over the Type that is being examined and asks you to return whether it should be excluded or not. This gives the caller much more scope to also point at a more complex function to determine whether to examine a control. In this case, we simply want to exclude Panel types so a simple anonymous delegate suffices.

foreach (Control c in Utils.GetControlsOfType<TextBoxBase>
	(this, delegate(Type MyType) { return MyType.Equals(typeof(Panel)); }))
{
     c.Text = "n/a";
}

Please note : Don't forget the constraint of 'where T : class'. We put this on to ensure that we will only deal with reference types. Otherwise the statement 'yield return (c as T)' would fail since casting the control c to type T can obviously only occur with Control types (which are reference types!).

Just a Word

With this example, I simply tried to bring across a different way of looking at tackling these kind of tasks.

Using it within a Form to find TextBoxes, you may not directly notice a speed or memory improvement but then again try using it for an iteration of List(s) that contains 1000s of entries and you'll soon spot the difference! :)

History

  • 1st June, 2007: Article published
  • 6th June, 2007: Article updated - statement change to (c is T) - thanks to PIEBALDconsult!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here