Using the controls collection recursively to find controls in the control hierarchy may cause unwanted side effects that may crash your ASP.NET application. It is concluded that the method HasControls is a safer way to check whether a control has child controls.
Often in ASP.Net there's a need during the Init phase of a page, before any viewstate properties are set, to check, add or change something somewhere in the control hierarchy. In PageAdapters this technic is also often used.
While developing I came across something strange I'll illustrate below in a simplified example.
Suppose you have a page with two controls like this. A gridview and a label that is used to display the selected item of the gridview.
<asp:GridView ID="EmployeesGrid" runat="server" AutoGenerateSelectButton="true">< FONT>asp:GridView>
<asp:Label ID="SelectedEmployee" runat="server" Text="">< FONT>asp:Label>
The VB code to fill the grid and display the selected item is pretty straightforward:
Private Sub Page_Load( ... ) Handles Me.Load
If Not IsPostBack Then
EmployeesGrid.DataSource = New String() {"Jerry", "Jane", "Theo", "Alice"}
EmployeesGrid.DataBind()
End If
End Sub
Private Sub EmployeesGrid_SelectedIndexChanged( .. ) Handles EmployeesGrid.SelectedIndexChanged
SelectedEmployee.Text = EmployeesGrid.SelectedRow.Cells(1).Text
End Sub
Suppose you want to add a tooltip to all employee controls. Using Page_Init the code could be like this:
Private Sub DoSomethingInTheControlHierarchy(ByVal Container As Control)
For Each c As Control In Container.Controls
If Not String.IsNullOrEmpty(c.ID) AndAlso _
c.ID.Contains("Employee") AndAlso _
TypeOf c Is WebControl Then
DirectCast(c, WebControl).ToolTip = "Has something to do with Employees"
End If
If c.Controls.Count > 0 Then
DoSomethingInTheControlHierarchy(c)
End If
Next
End Sub
Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Init
If Controls.Count > 0 Then
DoSomethingInTheControlHierarchy(Me)
End If
End Sub
Running this code and selecting some name in the grid results in this error:
Server Error in '/' Application.
Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
Source Error:
Line 29:
Line 30: Private Sub EmployeesGrid_SelectedIndexChanged ...
Line 31: SelectedEmployee.Text = _ EmployeesGrid.SelectedRow.Cells(1).Text
Line 32: End Sub
Line 33: End Class
|
Source File: C:\Users\Hugo\Documents\Visual Studio 2008\Projects\FindControls\FindControls\Default.aspx.vb Line: 31
It took a while to figure it out, because the code to traverse the control hierarchy works correctly. The problem is in the use of the Controls collection property used to check if a control has child controls (Controls.Count > 0). This property has some side effects that prevents the SelectedIndexChanged from working correctly. Which side effects I cannot figure out yet, but it probably screws up the viewstate of the page. In this simple sample code it's not crucial to use the Init event. I could write the same code in the Load event instead, in that case everything works allright.
What to do? Well, a Control also has a method called HasControls(). This method returns true when the control has child controls. When I replace Controls.Count > 0 to HasControls() the code that handles the SelectedIndexChanged event doesn't throw an error anymore.
Try it yourself! Below are two zip files containing a C# and VB version of the code using a the Controls property. Change Controls.Count to HasControls() to see it work without the error.
FindControlsCSharp.zip
FindControlsVB.zip