Introduction
All ASP.NET server controls include anID
property that
uniquely identifies the control and is the means by which the control is
programmatically accessed in the code-behind class. Similarly, the
elements in an HTML document may include an id
attribute that uniquely identifies the element; these id
values are often used in client-side script to programmatically
reference a particular HTML element. Given this, you may assume that
when an ASP.NET server control is rendered into HTML, its ID
value is used as the id
value of the rendered HTML element. This is not necessarily the case
because in certain circumstances a single control with a single ID
value may appear multiple times in the rendered markup. Consider a
GridView control that includes a TemplateField with a Label Web control
with an ID
value of ProductName. When the GridView is bound
to its data source at runtime, this Label is repeated once for every
GridView row. Each rendered Label needs a unique id
value.To handle such scenarios, ASP.NET allows certain controls to be denoted as naming containers. A naming container serves as a new
ID
namespace. Any server controls that appear within the naming container have their rendered id
value prefixed with the ID
of the naming container control. For example, the GridView
GridViewRow
classes are both naming containers. Consequently, a Label control defined in a GridView TemplateField with ID
ProductName is given a rendered id
value of GridViewID_GridViewRowID_ProductName
. Because GridViewRowID is unique for each GridView row, the resulting id
values are unique. and
Note: The
Naming containers not only change the rendered INamingContainer
interface is used to indicate that a particular ASP.NET server control should function as a naming container. The INamingContainer
interface does not spell out any methods that the server control must
implement; rather, it's used as a marker. In generating the rendered
markup, if a control implements this interface then the ASP.NET engine
automatically prefixes its ID
value to its descendents' rendered id
attribute values. This process is discussed in more detail in Step 2.id
attribute value, but also affect how the control may be programmatically
referenced from the ASP.NET page's code-behind class. The FindControl("controlID")
method is commonly used to programmatically reference a Web control. However, FindControl
does not penetrate through naming containers. Consequently, you cannot directly use the Page.FindControl
method to reference controls within a GridView or other naming container.As you may have surmised, master pages and ContentPlaceHolders are both implemented as naming containers. In this tutorial we examine how master pages affect HTML element
id
values and ways to programmatically reference Web controls within a content page using FindControl
.Step 1: Adding a New ASP.NET Page
To demonstrate the concepts discussed in this tutorial, let's add a new ASP.NET page to our website. Create a new content page namedIDIssues.aspx
in the root folder, binding it to the Site.master
master page. QuickLoginUI
and LeftColumnContent
ContentPlaceHolders contain suitable default markup for this page, go
ahead and remove their corresponding Content controls from IDIssues.aspx
. At this point, the content page's declarative markup should look like the following:<%@ Page Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true" CodeFile="IDIssues.aspx.cs" Inherits="IDIssues" Title="Untitled Page" %>
<asp:Content ID="Content1" ContentPlaceHolderID="head" Runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" Runat="Server">
</asp:Content>
In the Specifying the Title, Meta Tags, and Other HTML Headers in the Master Page tutorial we created a custom base page class (BasePage
) that automatically configures the page's title if it is not explicitly set. For the IDIssues.aspx
page to employ this functionality, the page's code-behind class must derive from the BasePage
class (instead of System.Web.UI.Page
). Modify the code-behind class's definition so that it looks like the following:public partial class IDIssues : BasePage
{
}
Finally, update the Web.sitemap
file to include an entry for this new lesson. Add a <siteMapNode>
title
and url
attributes to "Control ID Naming Issues" and ~/IDIssues.aspx
, respectively. After making this addition your Web.sitemap
file's markup should look similar to the following: element and set its <?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
<siteMapNode url="~/Default.aspx" title="Home">
<siteMapNode url="~/About.aspx" title="About the Author" />
<siteMapNode url="~/MultipleContentPlaceHolders.aspx" title="Using Multiple ContentPlaceHolder Controls" />
<siteMapNode url="~/Admin/Default.aspx" title="Rebasing URLs" />
<siteMapNode url="~/IDIssues.aspx" title="Control ID Naming Issues" />
</siteMapNode>
</siteMap>
As Figure 2 illustrates, the new site map entry in Web.sitemap
is immediately reflected in the Lessons section in the left column.
Step 2: Examining the Rendered ID
Changes
To better understand the modifications the ASP.NET engine makes to the rendered id
values of server controls, let's add a few Web controls to the IDIssues.aspx
page and then view the rendered markup sent to the browser.
Specifically, type in the text "Please enter your age:" followed by a
TextBox Web control. Further down on the page add a Button Web control
and a Label Web control. Set the TextBox's ID
and Columns
properties to Age
and 3, respectively. Set the Button's Text
and ID
properties to "Submit" and SubmitButton
. Clear out the Label's Text
property and set its ID
to Results
.At this point your Content control's declarative markup should look similar to the following:
<p>
Please enter your age:
<asp:TextBox ID="Age" Columns="3" runat="server"></asp:TextBox>
</p>
<p>
<asp:Button ID="SubmitButton" runat="server" Text="Submit" />
</p>
<p>
<asp:Label ID="Results" runat="server"></asp:Label>
</p>
Figure 3 shows the page when viewed through Visual Studio's designer.Visit the page through a browser and then view the HTML source. As the markup below shows, the
id
ID
ID
values of the naming containers in the page. values of the HTML elements for the TextBox, Button, and Label Web controls are a combination of the values of the Web controls and the <p>
Please enter your age:
<input name="ctl00$MainContent$Age" type="text" size="3" id="ctl00_MainContent_Age" />
</p>
<p>
<input type="submit" name="ctl00$MainContent$SubmitButton" value="Submit" id="ctl00_MainContent_SubmitButton" />
</p>
<p>
<span id="ctl00_MainContent_Results"></span>
</p>
As noted earlier in this tutorial, both the master page and its
ContentPlaceHolders serve as naming containers. Consequently, both
contribute the rendered ID
values of their nested controls. Take the TextBox's id
attribute, for instance: ctl00_MainContent_Age
. Recall that the TextBox control's ID
value was Age
. This is prefixed with its ContentPlaceHolder control's ID
value, MainContent
. Furthermore, this value is prefixed with the master page's ID
value, ctl00
. The net effect is an id
attribute value consisting of the ID
values of the master page, the ContentPlaceHolder control, and the TextBox itself.Figure 4 illustrates this behavior. To determine the rendered
id
of the Age
TextBox, start with the ID
value of the TextBox control, Age
.
Next, work your way up the control hierarchy. At each naming container
(those nodes with a peach color), prefix the current rendered id
with the naming container's id
.
Note: As we discussed, the
Because the master page itself serves as a naming container, the Web
controls defined in the master page also have altered rendered ctl00
portion of the rendered id
attribute constitutes the ID
value of the master page, but you may be wondering how this ID
value came about. We did not specify it anywhere in our master or
content page. Most server controls in an ASP.NET page are added
explicitly through the page's declarative markup. The MainContent
ContentPlaceHolder control was explicitly specified in the markup of Site.master
; the Age
TextBox was defined IDIssues.aspx
's markup. We can specify the ID
ID
values must be automatically generated for us. The ASP.NET engine sets the ID
values at runtime for those controls whose IDs have not been explicitly set. It uses the naming pattern ctlXX
, where XX is a sequentially increasing integer value.
values for these types of controls through the Properties window or
from the declarative syntax. Other controls, like the master page
itself, are not defined in the declarative markup. Consequently, their id
attribute values. For example, the DisplayDate
Label we added to the master page in the Creating a Site-Wide Layout with Master Pages tutorial has the following rendered markup:<span id="ctl00_DateDisplay">current date</span>
Note that the id
attribute includes both the master page's ID
value (ctl00
) and the ID
value of the Label Web control (DateDisplay
).
Step 3: Programmatically Referencing Web Controls via FindControl
Every ASP.NET server control includes a FindControl("controlID")
method that searches the control's descendents for a control named controlID. If such a control is found, it is returned; if no matching control is found, FindControl
returns null
.FindControl
is useful in scenarios where you need to
access a control but you don't have a direct reference to it. When
working with data Web controls like the GridView, for example, the
controls within the GridView's fields are defined once in the
declarative syntax, but at runtime an instance of the control is created
for each GridView row. Consequently, the controls generated at runtime
exist, but we do not have a direct reference available from the
code-behind class. As a result we need to use FindControl
to programmatically work with a specific control within the GridView's fields. (For more information on using FindControl
to access the controls within a data Web control's templates, see Custom Formatting Based Upon Data.) This same scenario occurs when dynamically adding Web controls to a Web Form, a topic discussed in Creating Dynamic Data Entry User Interfaces.To illustrate using the
FindControl
method to search for controls within a content page, create an event handler for the SubmitButton
's Click
event. In the event handler, add the following code, which programmatically references the Age
TextBox and Results
Label using the FindControl
method and then displays a message in Results
based on the user's input.
Note: Of course, we don't need to use
FindControl
to reference the Label and TextBox controls for this example. We could reference them directly via their ID
property values. I use FindControl
here to illustrate what happens when using FindControl
from a content page.protected void SubmitButton_Click(object sender, EventArgs e)
{
Label ResultsLabel = FindControl("Results") as Label;
TextBox AgeTextBox = Page.FindControl("Age") as TextBox;
ResultsLabel.Text = string.Format("You are {0} years old!", AgeTextBox.Text);
}
While the syntax used to call the FindControl
method differs slightly in the first two lines of SubmitButton_Click
, they are semantically equivalent. Recall that all ASP.NET server controls include a FindControl
method. This includes the Page
class, from which all ASP.NET code-behind classes must derive from. Therefore, calling FindControl("controlID")
is equivalent to calling Page.FindControl("controlID")
, assuming you haven't overridden the FindControl
method in your code-behind class or in a custom base class.After entering this code, visit the
IDIssues.aspx
page through a browser, enter your age, and click the "Submit" button. Upon clicking the "Submit" button a NullReferenceException
is raised (see Figure 5).If you set a breakpoint in the
SubmitButton_Click
event handler you will see that both calls to FindControl
return a null
value. The NullReferenceException
is raised when we attempt to access the Age
TextBox's Text
property.The problem is that
Control.FindControl
only searches Control's descendents that are in the same naming container. Because the master page constitutes a new naming container, a call to Page.FindControl("controlID")
never permeates the master page object ctl00
. (Refer back to Figure 4 to view the control hierarchy, which shows the Page
object as the parent of the master page object ctl00
.) Therefore, the Results
Label and Age
TextBox are not found and ResultsLabel
and AgeTextBox
are assigned values of null
. There are two workarounds to this challenge: we can drill down, one naming container at a time, to the appropriate control; or we can create our own
FindControl
method that permeates naming containers. Let's examine each of these options.Drilling Into the Appropriate Naming Container
To useFindControl
to reference the Results
Label or Age
TextBox, we need to call FindControl
MainContent
Results
or Age
that is within the same naming container. In other words, calling the FindControl
method from the MainContent
control, as shown in the code snippet below, correctly returns a reference to the Results
or Age
controls. from an ancestor control in the same naming container. As Figure 4 showed, the ContentPlaceHolder control is the only ancestor of Label ResultsLabel = MainContent.FindControl("Results") as Label;
TextBox AgeTextBox = MainContent.FindControl("Age") as TextBox;
However, we cannot work with the MainContent
ContentPlaceHolder from our content page's code-behind class using the
above syntax because the ContentPlaceHolder is defined in the master
page. Instead, we have to use FindControl
to get a reference to MainContent
. Replace the code in the SubmitButton_Click
event handler with the following modifications:protected void SubmitButton_Click(object sender, EventArgs e)
{
ContentPlaceHolder MainContent = FindControl("MainContent") as ContentPlaceHolder;
Label ResultsLabel = MainContent.FindControl("Results") as Label;
TextBox AgeTextBox = MainContent.FindControl("Age") as TextBox;
ResultsLabel.Text = string.Format("You are {0} years old!", AgeTextBox.Text);
}
If you visit the page through a browser, enter your age, and click the "Submit" button, a NullReferenceException
is raised. If you set a breakpoint in the SubmitButton_Click
event handler you will see that this exception occurs when attempting to call the MainContent
object's FindControl
MainContent
object is null
because the FindControl
method cannot locate an object named "MainContent". The underlying reason is the same as with the Results
Label and Age
TextBox controls: FindControl
starts its search from the top of the control hierarchy and does not penetrate naming containers, but the MainContent
ContentPlaceHolder is within the master page, which is a naming container. method. The Before we can use
FindControl
to get a reference to MainContent
,
we first need a reference to the master page control. Once we have a
reference to the master page we can get a reference to the MainContent
ContentPlaceHolder via FindControl
and, from there, references to the Results
Label and Age
TextBox (again, through using FindControl
). But how do we get a reference to the master page? By inspecting the id
attributes in the rendered markup it's evident that the master page's ID
value is ctl00
. Therefore, we could use Page.FindControl("ctl00")
to get a reference to the master page, then use that object to get a reference to MainContent
, and so on. The following snippet illustrates this logic:// Get a reference to the master page
MasterPage ctl00 = FindControl("ctl00") as MasterPage;
// Get a reference to the ContentPlaceHolder
ContentPlaceHolder MainContent = ctl00.FindControl("MainContent") as ContentPlaceHolder;
// Reference the Label and TextBox controls
Label ResultsLabel = MainContent.FindControl("Results") as Label;
TextBox AgeTextBox = MainContent.FindControl("Age") as TextBox;
While this code will certainly work, it assumes that the master page's autogenerated ID
will always be ctl00
. It's never a good idea to make assumptions about autogenerated values.Fortunately, a reference to the master page is accessible through the
Page
class's Master
property. Therefore, instead of having to use FindControl("ctl00")
to get a reference of the master page in order to access the MainContent
ContentPlaceHolder, we can instead use Page.Master.FindControl("MainContent")
. Update the SubmitButton_Click
event handler with the following code:protected void SubmitButton_Click(object sender, EventArgs e)
{
ContentPlaceHolder MainContent = Page.Master.FindControl("MainContent") as ContentPlaceHolder;
Label ResultsLabel = MainContent.FindControl("Results") as Label;
TextBox AgeTextBox = MainContent.FindControl("Age") as TextBox;
ResultsLabel.Text = string.Format("You are {0} years old!", AgeTextBox.Text);
}
This time, visiting the page through a browser, entering your age, and clicking the "Submit" button displays the message in the Results
Label, as expected.Recursively Searching Through Naming Containers
The reason the previous code example referenced theMainContent
ContentPlaceHolder control from the master page, and then the Results
Label and Age
TextBox controls from MainContent
, is because the Control.FindControl
method only searches within Control's naming container. Having FindControl
ID
values. Consider the case of a GridView that defines a Label Web control named ProductName
within one of its TemplateFields. When the data is bound to the GridView at runtime, a ProductName
Label is created for each GridView row. If FindControl
searched through all naming containers and we called Page.FindControl("ProductName")
, what Label instance should the FindControl
return? The ProductName
Label in the first GridView row? The one in the last row?
stay within the naming container makes sense in most scenarios because
two controls in two different naming containers may have the same So having
Control.FindControl
search just Control's naming container makes sense in most cases. But there are other cases, such as the one facing us, where we have a unique ID
across all naming containers and want to avoid having to meticulously
reference each naming container in the control hierarchy to access a
control. Having a FindControl
variant that recursively
searches all naming containers makes sense, too. Unfortunately, the .NET
Framework does not include such a method.The good news is that we can create our own
FindControl
method that recursively searches all naming containers. In fact, using extension methods we can tack on a FindControlRecursive
method to the Control
class to accompany its existing FindControl
method.
Note: Extension methods are a
feature new to C# 3.0 and Visual Basic 9, which are the languages that
ship with the .NET Framework version 3.5 and Visual Studio 2008. In
short, extension methods allow for a developer to create a new method
for an existing class type through a special syntax. For more
information on this helpful feature, refer to my article, Extending Base Type Functionality with Extension Methods.
To create the extension method, add a new file to the App_Code
folder named PageExtensionMethods.cs
. Add an extension method named FindControlRecursive
that takes as an input a string
parameter named controlID
. For extension methods to work properly, it is vital that the class itself and its extension methods be marked static
.
Moreover, all extension methods must accept as their first parameter an
object of the type to which the extension method applies, and this
input parameter must be preceded with the keyword this
.Add the following code to the
PageExtensionMethods.cs
class file to define this class and the FindControlRecursive
extension method:using System;
using System.Web;
using System.Web.UI;
public static class PageExtensionMethods
{
public static Control FindControlRecursive(this Control ctrl, string controlID)
{
if (string.Compare(ctrl.ID, controlID, true) == 0)
{
// We found the control!
return ctrl;
}
else
{
// Recurse through ctrl's Controls collections
foreach (Control child in ctrl.Controls)
{
Control lookFor = FindControlRecursive(child, controlID);
if (lookFor != null)
return lookFor; // We found the control
}
// If we reach here, control was not found
return null;
}
}
}
With this code in place, return to the IDIssues.aspx
page's code-behind class and comment out the current FindControl
method calls. Replace them with calls to Page.FindControlRecursive("controlID")
.
What's neat about extension methods is that they appear directly within
the IntelliSense drop-down lists. As Figure 7 shows, when you type Page
and then hit period, the FindControlRecursive
method is included in the IntelliSense drop-down along with the other Control
class methods.Enter the following code into the
SubmitButton_Click
event handler and then test it by visiting the page, entering your age,
and clicking the "Submit" button. As shown back in Figure 6, the
resulting output will be the message, "You are age years old!" protected void SubmitButton_Click(object sender, EventArgs e)
{
Label ResultsLabel = Page.FindControlRecursive("Results") as Label;
TextBox AgeTextBox = Page.FindControlRecursive("Age") as TextBox;
ResultsLabel.Text = string.Format("You are {0} years old!", AgeTextBox.Text);
}
Note: Because extension methods
are new to C# 3.0 and Visual Basic 9, if you are using Visual Studio
2005 you cannot use extension methods. Instead, you'll need to implement
the
FindControlRecursive
Rick Strahl has such an example in his blog post, ASP.NET Maser Pages and FindControl
. method in a helper class.
Step 4: Using the Correct id
Attribute Value in Client-Side Script
As noted in this tutorial's introduction, a Web control's rendered id
attribute is oftentimes used in client-side script to programmatically
reference a particular HTML element. For example, the following
JavaScript references an HTML element by its id
and then displays its value in a modal message box:var elem = document.getElementById("Age");
if (elem != null)
alert("You entered " + elem.value + " into the Age text box.");
Recall that in ASP.NET pages that do not include a naming container, the rendered HTML element's id
ID
property value. Because of this, it is tempting to hard code in id
Age
TextBox Web control through client-side script, do so via a call to document.getElementById("Age")
. attribute is identical to the Web control's attribute values into JavaScript code. That is, if you know you want to access the The problem with this approach is that when using master pages (or other naming container controls), the rendered HTML
id
is not synonymous with the Web control's ID
property. Your first inclination may be to visit the page through a browser and view the source to determine the actual id
attribute. Once you know the rendered id
value, you can paste it into the call to getElementById
to access the HTML element you need to work with through client-side
script. This approach is less than ideal because certain changes to the
page's control hierarchy or changes to the ID
properties of the naming controls will alter the resulting id
attribute, thereby breaking your JavaScript code.The good news is that the
id
attribute value that is rendered is accessible in server-side code through the Web control's ClientID
property. You should use this property to determine the id
attribute value used in client-side script. For example, to add a
JavaScript function to the page that, when called, displays the value of
the Age
TextBox in a modal message box, add the following code to the Page_Load
event handler:ClientScript.RegisterClientScriptBlock(this.GetType(), "ShowAgeTextBoxScript",
string.Format(@"function ShowAge()
{{
var elem = document.getElementById('{0}');
if (elem != null)
alert('You entered ' + elem.value + ' into the Age text box.');
}}", AgeTextBox.ClientID), true);
The above code injects the value of the Age
TextBox's ClientID property into the JavaScript call to getElementById
. If you visit this page through a browser and view the HTML source, you'll find the following JavaScript code:<script type="text/javascript">
//<![CDATA[
function ShowAge()
{
var elem = document.getElementById('ctl00_MainContent_Age');
if (elem != null)
alert('You entered ' + elem.value + ' into the Age text box.');
}//]]>
</script>
Notice how the correct id
attribute value, ctl00_MainContent_Age
, appears within the call to getElementById
. Because this value is calculated at runtime, it works regardless of later changes to the page control hierarchy.
Note: This JavaScript example
merely shows how to add a JavaScript function that correctly references
the HTML element rendered by a server control. To use this function you
would need to author additional JavaScript to call the function when the
document loads or when some specific user action transpires. For more
information on these and related topics, read Working with Client-Side Script.
No comments :
Post a Comment