Oct 11th 2010 09:11 am
Finding Controls and Wiring Events Without Recursive FindControl
When using templated controls, one common problem is locating specific controls inside the template to set properties and attach handlers. Consider the Wizardcontrol exposes a templated property to override the Finish Navigation Template. Suppose you use the following template:
<FinishNavigationTemplate> Please enter your email address if you would like a confirmation email: <asp:TextBox ID="emailTextBox" Runat="server"> </asp:TextBox> <br /> <asp:Button CommandName="MovePrevious" ID="btnMovePrevious" Runat="server" Text="Previous" /> <asp:Button CommandName="MoveComplete" ID="btnComplete" Runat="server" Text="Finish" /> </FinishNavigationTemplate>
If you want to control the visibility of the “Finish” button, you need some way to get a reference to the “btnComplete” control.
There’s a similar problem when using the Login control. The root of the problem stems from the way controls are instantiated inside template containers and there is no page level reference created for these controls.
One relatively common pattern is to isolate the control hierarchy created by the container e.g. this.Controls[0].Controls[1]….Controls[3] as Button. This creates a tight coupling between the control hierarchy created by the templated control and the consumer of the control – a violation of the Dependency Inversion Principle.
Another approach is to override the FindControlmethod to make it recursive. This is sort of a sledgehammer approach that is less brittle but also imprecise. In the example above, if there were two controls in the wizard’s control hierarchy named “btnComplete”, it is unclear which reference would be returned by the method.
Generally developers need recursive FindControl for the following reasons:
- To attach to events before the framework processes the Request.Form’s “__EVENTTARGET” and “__EVENTARGUMENT” properties to call the appropriate server side methods.
- To set control properties before rendering (usually in the PreRender event)
The Microsoft answer to the first problem is to use the OnBubble Event. To quote Microsoft:
ASP.NET server controls such as the Repeater, DataList and GridView Web controls can contain child controls that raise events. For example, each row in a GridView control can contain one or more buttons created dynamically by templates. Rather than each button raising an event individually, events from the nested controls are “bubbled” – that is, they are sent to the naming container. The naming container in turn raises a generic event called RowCommand with parameter values. These values allow you to determine which individual control that raised the original event. By responding to this single event, you can avoid having to write individual event-handling methods for child controls.
Rather than attach to a control’s specific Event like “OnClick”, one can attach to the OnBubble event of a container control (like the template itself). This is how the FinishNavigationTemplate example above works. The Wizard control overrides its own OnBubble event and looks for a CommandName “MoveComplete”. In this way the Wizard control can listen for a “MoveComplete” Command and raise it’s own “OnFinishButtonClick” event.
protected override bool OnBubbleEvent(object source, EventArgs e)
{
bool flag = false;
CommandEventArgs args = e as CommandEventArgs;
if (args != null)
{
...
else if (string.Equals(MoveCompleteCommandName, args.CommandName, StringComparison.OrdinalIgnoreCase))
{
...
this.OnFinishButtonClick(args2);
...
}
}
The second reason is a bit more complicated to answer. The Asp.Net Framework maintains two different data structures to traverse a control’s child control collection. The first structure is a simple collection of controls to be rendered. This collection is exposed via the “Controls” property. When executing a recursive version of FindControl this is the collection traversed recursively.
The default implementation of FindControl uses the second structure to locate controls at arbitrary depths in the hierarchy. This data structure contiains a dictionary whose key is the control id and value is the control itself. The dictionary is composed only of controls that implement INamingContainer. Starting with the current control’s naming container, it searches through the child naming containers’ child control collections.
The following is part of the decompiled code for the FindControl method (inline comments are mine):
protected virtual Control FindControl(string id, int pathOffset)
{
...
char[] anyOf = new char[] { '$', ':' };
int num = id.IndexOfAny(anyOf, pathOffset);
if (num == -1)
{
//The identifier passed in doesn't contain any container delimiter characters
//so search its name/control dictionary and return the result
str = id.Substring(pathOffset);
return (this._occasionalFields.NamedControls[str] as Control);
}
//The identifier passed in does contain a delimiter character
//get the identifier and find a control in the
// name/control dictionary
str = id.Substring(pathOffset, num - pathOffset);
Control control2 = this._occasionalFields.NamedControls[str] as Control;
if (control2 == null)
{
return null;
}
//call the child naming container's FindControl method
return control2.FindControl(id, num + 1);
}
For the FinishNavigationTemplate example above we can find the control we’re looking for by calling wizard.FindControl(“FinishNavigationTemplateContainerID$btnComplete”);.
If the wizard control had ID “MyWizard” and were contained inside a panel that implemented INamingContainer, we could instead call panel.FindControl(“MyWizard$FinishNavigationTemplateContainer$btnComplete”);. The control “btnComplete” doesn’t have to be a first level child inside the template. In this case, however, there could be no controls implementing INamingContainer between the template and the button – otherwise we’d have to include them in the argument passed to FindControl e.g. “MyWizard$FinishNavigationTemplateContainer$SomeContainer$btnComplete”.
One more note on using FindControl within templated pages. When calling FindControl from the codebehind of a page that has a MasterPage, the method described above doesn’t work as one would expect. Instead of the naming container page one expects “Page$Control” the framework uses the Body and ContentPlaceholder controls too. The full path to a control is more likely “Body$ContenPlaceHolder1$Control”. From within a templated page the body and contentplaceholder id’s can get in the way at first.
No Comments yet »