Tuesday, October 30, 2012

How do I use FindControl() in ASP.NET? When should I use FindControl()?

When coding ASP.NET pages, it often becomes necessary to call on a control from the code-behind page that is not directly available. Your first hint that you are in this situation is when you know the name of the control you are trying to access, but it does not show up in Visual Studio's Intellisense dropdown. Assuming you are actually calling the control by its correct ID (double-check!) then the reason it does not show up in Intellisense (and why you cannot call on it directly from the ASP.NET code-behind file) is usually because the control is housed inside some other structure.
There are several situations that can cause this, including but not limited to these three:
  1. Calling on a control, such as a textbox, that you defined on the page that used a CrossPage Postback to load this page.
  2. Calling on a control, such as a label, that you defined in the MasterPage associated with this content page.
  3. Calling on a control, such as a checkbox, that you defined in a templated control of a DetailsView on this page.
In all of these situations, the control you want to access is actually being built or housed by another control. To access it, you need to start by calling on the item you can access directly from the code-behind, then drill down into it using the FindControl() method.

The FindControl() method requires a string argument to be passed with the exact ID of the control being sought. A new Control object is returned in memory, with all the same properties and values of the one that was found. So you can use it to get the control's information or manipulate the control as needed.

Common Trip-ups

In case you don't call on the string ID properly (no Intellisense help for that!) or if under the current runtime circumstances, there is no value whatsoever for the Control you seek, you will run into a NullReferenceException. The Message property for this exception is "Object reference not set to an instance of an object". You can avoid this by first testing to make sure the control you are looking to call FindControl() from exists before trying to use it to call the method.

Keep in mind that because the FindControl() method returns a Control object, you will usually need to unbox that object to the more specific type of control you were actually looking for. So in case 1, cast it back to a TextBox. In case 2, cast it back to a DropDownList. In case 3, cast it to a CheckBox.

Also, realize that it is possible that the structure you need to dig through may be more complicated than it at first appears. Here is some sample code using FindControl() for cases 1-3, followed by a 4th example and the code it would require.

Case 1: TextBox1 available from PreviousPage property because it was passed using CrossPage Postback

protected void Page_Load(object sender, EventArgs e)
    {
        /*In a cross-page postback, be sure to test if
         * the page loaded with PreviousPage info before trying
         * to use it. If it's null, inform user of problem.
         * */
        if (PreviousPage != null)
        {
            TextBox tbName =
                (TextBox) PreviousPage.FindControl("TextBox1");
            lblName.Text = tbName.Text;
        }
        else
        {
            //Notify the user to start on the right page
            lblName.Text = "Please retry from the correct page.";
        }
    }


Case 2: DropDownList1 available from Master property because it was generated there for the Content Page

protected void Page_Load(object sender, EventArgs e)
    {
        /*Access the Label1 defined on master page from this
        * content page and assign its Text property. Each
        * content page will have unique content for this
        * label on the masterpage.
        * */
        Label lblPage = (Label)Master.FindControl("Label1");
        if (lblPage != null)
        {
            lblPage.Text = "Northwind Products Catalog";
        }
    }


Case 3: Checkbox1 available from DetailsView1 that generates it

protected void CheckBox1_PreRender(object sender, EventArgs e)
    {
        //waits until PreRender event to check the checkbox
        CheckBox cbIsActive =
            (CheckBox)DetailsView1.FindControl("CheckBox1");
        cbIsActive.Checked = true;
    }


Case 4: TextBox1 available from PreviousPage property due to CrossPagePostback. But because a MasterPage is also involved (the posted back page is a Content Page) the FindControl() path is more elaborate.

protected void Page_Load(object sender, EventArgs e)
    {
        /*Since we are involved with a masterpage, but we want
        * info on a control in PreviousPage (because of
        * crosspage postback or Server.Transfer), go through:
        * > PreviousPage> Form> ContentPlaceholder> desired control
        * */
       if(PreviousPage != null)
       {
            TextBox tbName = (TextBox)PreviousPage.Form.
            FindControl("ContentPlaceholder1").
            FindControl("TextBox1");
        }
        else
        {
            //Notify the user to start on the right page
            lblName.Text = "Please retry from the correct page.";
        }
    }   


The 4th example introduces a wrinkle because of the additional complexity of a crosspage postback combined with a masterpage. For an excellent write-up of how this actually gets built behind the scenes (and why you need to go through the PreviousPage's Form, then find its ContentPlaceHolder before you can find the TextBox inside it, see K. Scott Allen's OdeToCode.com Master Page article which includes the image below.)
Ode to Code Master Page hierarchy diagram

These types of wrinkles come up now and again when using FindControl() with more elaborate structures or controls housed inside of other controls, some of which may not be immediately apparent to you. Picking them apart is all part of furthering your capabilities with this method.

INamingContainer Sidebar

As a last technical sidenote, structures that wind up defining their own ID namespace on the page for controls housed inside them tend to implement the INamingContainer interface - meaning they must implement functionality to do just that. This is done to ensure that default ID's, like "Label1", aren't used repeatedly because some structure is automatically generating them. So just because a DetailsView builds label controls within its structure, we don't want it to generate an ID that might already being generated or used elsewhere on the page. So the DetailsView uses a naming convention for each label it generates to associate it with the control that created it – here, the DetailsView.

A standard ASP:Label control with a default ID of "Label1" would be adaptively rendered into HTML as:
<span id="Label1">Dynamically-Assigned Text Here</span>

Whereas a label control generated by a DetailsView with a default ID of "DetailsView1" would be adaptively rendered into HTML as:
<span id="DetailsView1_Label1">Dynamically-Assigned Text Here</span>

For more information, see also the MSDN information regarding the INamingContainerinterface.

At the end of the day, the FindControl() is a staple in your ASP.NET toolbox, and finding useful opportunities to employ it will improve your web development skillset.

2 comments: