A Very Efficient Generic Recursive Iterator using Yield Return





4.00/5 (19 votes)
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 TextBox
es on your form to show "n/a". So you will have to recursively find all TextBox
es 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
Button
s instead ofTextBox
es within yourForm
? 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 aList
with ALL the items only to set aText
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 TextBox
es 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 Type
s 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 TextBox
es, 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
isT
) - thanks to PIEBALDconsult!