Background
There are many situations in which a set of actions must be applied to a particular type of control on an ASP.Net web page. The most obvious way to accomplish this is to simply list all of the specific controls and perform the action. That will certainly work and performs quickly, but it requires changes if another one of those control types is added to the page in the future (most likely by someone else). This article describes a more general mechanism that applies a given set of changes to a particular type of control on a page.
ASP.Net pages are constructed using HTML, a set of built-in server controls (asp:TextBox, asp:Label, etc) and custom created user controls. As the ASP.Net page is executed, the runtime creates an instance of the System.Web.UI.Page class. Server and user controls that are placed on the page are instantiated as objects and added to the "Controls" property of the Page instance (which is an instance of the System.Web.UI.ControlCollection class). This collection of controls is used to create the output of the page that is sent to the client as HTML. A page with two labels, two text boxes, and a button had the following control hiearchy:
The first entry is the page instance itself (asp.WebForm2_aspx). The next item is the literal HTML text that was in the sample page, which was placed into a System.Web.UI.ResourceBaseLiteralControl (_ctl0). The remainder of the items are the form, web controls, and literal text on the page.
Given the controls collection it is possible to "walk" the control hierarchy tree and apply specific actions to certain types of controls.
Progressive Solution
At first glance it seems that using the foreach statement to loop through the Controls property might work, but it does not because the controls are actually a tree where top level controls include other controls as children. For example, the form control is actually a parent for all of the HTML contained within it (text boxes, labels, etc).
The next series of functions and descriptions show a series of more detailed examples leading up to the eventual solution. Each shows a slightly more advanced version and shows the how it would be called. To test these functions, create a web page (assume webForm1.aspx) that uses "FlowLayout" for the pageLaout (Visual Studio.Net defaults to GridLayout for pages. Change the pageLayout properties for the page in design view to "FlowLayout" or simply remove the
MS_POSITIONING="GridLayout" using the HTML view on the <body> tag). Add some various controls and HTML to the page (include at least one TextBox as they are used in the samples) and call the proper function from the Page_Load( ) as shown in each example. The functions are present in the helper.cs file included in the article files for
download.
SimpleRecursion
One approach to walking tree structures is to create a recursive function. Recursion is a programming technique where a function calls itself. Here is a simple recursive function that walks the entire tree (depth first) and displays the type of each control:
helper.cs
public static void SimpleRecursion(System.Web.UI.Control oControl)
{
System.Web.HttpContext.Current.Response.Write(oControl.GetType().ToString() + "<br>");
foreach (System.Web.UI.Control oChildControl in oControl.Controls)
{
SimpleRecursion(oChildControl);
}
}
WebForm1.aspx.cs
Page_Load:
TestControlHierarchyProcessing.helper.SimpleRecursion(Page);
SimpleRecursionTextBoxes
The next step is to check the type of the control and decide if it should be processed. .Net provides the ability to determine the type of a given object, and compare it to a specified type. An easy way to do this is to use the
is operator. This checks the type of the object and only does the work for controls that are a TextBox:
helper.cs
public static void SimpleRecursionTextBoxes(System.Web.UI.Control oControl)
{
if (oControl is System.Web.UI.WebControls.TextBox)
{
System.Web.HttpContext.Current.Response.Write(oControl.GetType().ToString() + "<br>");
}
foreach (System.Web.UI.Control oChildControl in oControl.Controls)
{
SimpleRecursionTextBoxes(oChildControl);
}
}
WebForm1.aspx.cs
Page_Load:
TestControlHierarchyProcessing.helper.SimpleRecursionTextBoxes(Page);
This works well, but it requires us to create a new recursive function for each unique type of control that we might want to process. How can we pass a type to the function as a parameter?
SimpleRecursionTyped
One method for passing the type information is to use a parameter with type System.Type. Here is the next version:
helper.cs
public static void SimpleRecursionTyped(System.Web.UI.Control oControl, System.Type oType)
{
if (oControl.GetType() == oType)
{
System.Web.HttpContext.Current.Response.Write(oControl.GetType().ToString() + "<br>");
}
foreach (System.Web.UI.Control oChildControl in oControl.Controls)
{
SimpleRecursionTyped(oChildControl, oType);
}
}
WebForm1.aspx.cs
Page_Load:
TestControlHierarchyProcessing.helper.SimpleRecursionTyped(Page,
typeof(System.Web.UI.WebControls.TextBox));
Now the function can be called with a generic type passed as a parameter. The GetType() method will return the exact type of the control, and it must match
exactly the type provided. It does NOT match if the class derives from the given type. So, for instance, a control of type System.Web.UI.WebControls.TextBox does not match a System.Web.UI.Control type even though it derives from it, but the
is operator would return true in that same case.
SimpleRecursionTypedOrDerived
This version provides a parameter that is used to specify whether the type checked should be for the exact class specified or can be a derived class:
helper.cs
public static void SimpleRecursionTypedOrDerived(System.Web.UI.Control oControl,
System.Type oType, bool exactType)
{
if (((exactType == true) && (oControl.GetType() == oType)) ||
(oType.IsInstanceOfType(oControl)))
{
System.Web.HttpContext.Current.Response.Write(oControl.GetType().ToString() + "<br>");
}
foreach (System.Web.UI.Control oChildControl in oControl.Controls)
{
SimpleRecursionTypedOrDerived(oChildControl, oType, exactType);
}
}
WebForm1.aspx.cs
Page_Load:
TestControlHierarchyProcessing.helper.SimpleRecursionTypedOrDerived(Page,
typeof(System.Web.UI.Control), false);
The call shown would request that any class that derives from System.Web.UI.Control would match and be displayed. The IsInstanceOfType() method checks if the control is the given type or derives from that type. This provides a generic function that can display the type of controls for any type of control in the control hierarchy, but what if we want to perform other actions?
SimpleRecursionTypedOrDerivedDelegate
This version introduces the concept of a delegate to provide a mechanism to invoke a method for each control that matches the desired type. A delegate defines the signature that a method must have (it is a "type-safe function pointer" for those with C/C++ backgrounds). It allows the caller to create a custom function and pass the location of that function to this function so that it can be invoked. The delegate enforces that the given method takes the parameters and returns the same type as the delegate. The highlighted delegate defines a method that takes the control instance as its only parameter and does not return anything. The second highlighted area invokes the method that was passed to the function after ensuring that the method was passed. The end result is that for each control that matches the given type the method defined by the caller will be invoked and the matching control will be passed. In this case, the DumpControlInfoNoArgs() method will be called (even though it is listed as "private", it will be invoked because it was passed to this function).
helper.cs
public delegate void ProcessControlDelegateNoArgs(System.Web.UI.Control oControl);
public static void SimpleRecursionTypedOrDerivedDelegate(System.Web.UI.Control oControl,
System.Type oType, ProcessControlDelegateNoArgs controlMethod, bool exactType)
{
if (((exactType == true) && (oControl.GetType() == oType)) ||
(oType.IsInstanceOfType(oControl)))
{
if (null != controlMethod)
{
controlMethod(oControl);
}
}
foreach (System.Web.UI.Control oChildControl in oControl.Controls)
{
SimpleRecursionTypedOrDerived(oChildControl, oType, exactType);
}
}
WebForm1.aspx.cs
Page_Load:
TestControlHierarchyProcessing.helper.SimpleRecursionTypedOrDerivedDelegate(Page,
typeof(Control),
new TestControlHierarchyProcessing.helper.ProcessControlDelegateNoArgs(DumpControlInfoNoArgs),
true);
private void DumpControlInfoNoArgs(System.Web.UI.Control oControl)
{
Response.Write(oControl.GetType().ToString() + "<br>");
}
Now the function can be reused, each caller can provide a different method that will be invoked for each control of a particular type. What if we need to pass additional parameters to the method?
Final Solution
The final version adds the ability to pass arbitrary arguments provided by the original caller to the method that will be invoked. The idea is to create a class hierarchy for the parameters. The base class DelegateArgs does not contain any members, it is provided for those methods that don't require any additional parameters. The class BooleanArgs contains a single bool variable, it uses a property for encapsulation purposes (never a good idea to allow direct access to member variables). The constructor takes the parameter value desired. Each unique combination of desired parameters for callable methods would need a new class defined that derives from DelegateArgs. Why bother with all of this? C# is strongly typed, and our method has to take a parameter of a specific type, but our methods may want to pass all sorts of different classes that store the parameter values. By using a class hierarchy, an instance of the BooleanArgs class can be passed as a DelegateArgs because it derives from that class.
The final solution also provides a depth parameter that is common for tree traversal recursive functions, it provides access to the "level" of the tree the control is present at.
The sample HandleVisibility() method demonstrates the usefullness of the BooleanArgs parameter, it allows reuse of the function for the purpose of either making the text boxes visible or invisible allowing the caller to pass the desired value through the generic HandleControl() method to the invoked delegated method. The type safety for the parameters passed is provided by the cast in the HandleVisibility() method, if the caller did not properly provide a BooleanArgs class originally, the cast would fail.
helper.cs
//Base class for all argument types
public class DelegateArgs
{
public DelegateArgs()
{
}
}
//Example set of arguments for a delegate, this is a single boolean
public class BooleanArgs : DelegateArgs
{
//Define parameters for delegate
private bool boolParam;
//Property to allow access to parameter info
public bool BoolParam
{
get
{
return boolParam;
}
}
//Constructor takes all parameter values to ensure they are all set
public BooleanArgs(bool paramValue)
{
boolParam = paramValue;
}
}
public delegate void ProcessControlDelegate(System.Web.UI.Control oControl,
DelegateArgs delegateParam, int depth);
public static void HandleControl(System.Web.UI.Control oControl, System.Type oType,
ProcessControlDelegate controlMethod, DelegateArgs delegateParam, bool exactType, int depth)
{
if (((exactType == true) && (oControl.GetType() == oType)) ||
(oType.IsInstanceOfType(oControl)))
{
if (null != controlMethod)
{
controlMethod(oControl, delegateParam, depth);
}
}
//Adjust the depth for the children of this call...
depth += 1;
foreach (System.Web.UI.Control oChildControl in oControl.Controls)
{
HandleControl(oChildControl, oType, controlMethod, delegateParam, exactType, depth);
}
}
WebForm1.aspx.cs
Page_Load:
TestControlHierarchyProcessing.helper.HandleControl(Page, typeof(TextBox),
new TestControlHierarchyProcessing.helper.ProcessControlDelegate(HandleVisibility),
new TestControlHierarchyProcessing.helper.BooleanArgs(false), false, 0);
private void HandleVisibility(System.Web.UI.Control oControl,
TestControlHierarchyProcessing.helper.DelegateArgs noArg, int depth)
{
TestControlHierarchyProcessing.helper.BooleanArgs oArgs =
(TestControlHierarchyProcessing.helper.BooleanArgs)noArg;
oControl.Visible = oArgs.BoolParam;
}
Conclusion
There are many occasions where the ability to apply a set of changes to a specific type of controls is useful. The final solution showed a generic method that can walk the control hierarchy and invoke a method that can take any type of parameters. There are no limits to how this can be used since the invoked method can use the control in any way it desires. We have used it to disable all text boxes on a web page when the user should have a readonly view of data while some other users need the ability to edit the fields. Since the ASP.Net controls themselves form a hierarchy, it is possible to pass System.Web.UI.WebControls.BaseDataList as the type of control to look for which would process both DataGrid and DataList items. The examples shown also pass the Page instance and pass the Control type which will match all controls on the page.
It is also not limited to starting with the controls at the page level, if you have a DataGrid that includes other controls that you want to process, just pass the DataGrid control instance as the root instead of the page, and the methods will be invoked for the DataGrid control and its children in the hierarchy.
Send comments or questions to robertb@aspalliance.com.