views:

758

answers:

3

When a content placeholder contains any code blocks it reports that the control collection is empty.

For instance:

MasterPage.aspx

<asp:ContentPlaceHolder ID="Content1" runat="server" />
<asp:ContentPlaceHolder ID="Content2" runat="server" />

<div>Content1: <%= Content1.Controls.Count %></div>
<div>Content2: <%= Content2.Controls.Count %></div>

APage.aspx

<asp:Content ContentPlaceHolderID="Content1" runat="server">
    Plain text content.
</asp:Content>

<asp:Content ContentPlaceHolderID="Content2" runat="server">
    <%= "Code block content." %>
</asp:Content>

This will render the following:

Plain text content. Code block content.

Content1: 1

Content2: 0

Why is the master page's ContentPlaceHolder.Controls collection empty?

I want to check whether the ContentPlaceHolder has been populated (see also this question) but can't if it contains any <%= blocks.

Does anyone know a way around this?

+2  A: 

The controls collection is empty because when <%= %> script tags are present, literal controls are not added to the control tree. However, server controls will still get added. So:

<asp:Content ID="Content2" ContentPlaceHolderID="Content2" Runat="Server">
     <%= "Code block content." %>
     <asp:GridView runat="server" ID="gvTest" />
</asp:Content>

<div>Content2: <%= Content2.Controls.Count %></div>

will return

Content 2: 1

Rick Strahl has a great article that explains this behavior:

To make code like this work, ASP.NET needs to override the rendering of the particular container in which any script code is hosted. It does this by using SetRenderMethodDelegate on the container and creating a custom rendering method ...

Rather than building up the control tree literal controls, ASP.NET only adds server controls to the control tree when <% %> tags are present for a container. To handle the literal content and the script markup, ASP.NET generates a custom rendering method. This method then explicitly writes out any static HTML content and any script expressions using an HTML TextWriter. Any script code (<% %>) is generated as raw code of the method itself.

Unfortunately I can't think of any elegant solution to this conundrum.

Chris Pebble
Nice explanation. That's a pain for MVC content though - I don't want to clutter <% %> content with <asp:Controls> just so that the Master page can see that the content panel has been used. There has to be some way round this.
Keith
+4  A: 

Hi Keith,

As promised, I said I would take a look. Sorry I never uploaded last night, long day and needed to hit the hay!

So, I was checking out the ContentPlaceHolder.Controls collection differences between how they are populated. I noticed that when the code block is used, it flips to read only. At any other point, it will simply be empty or populated.

I therefore decided to throw in an extension method to check it for us:

ContentPlaceHolderExtensions.cs

public static class ContentPlaceHolderExtensions
{
    public static bool ContainsControlsOrCodeBlock(this ContentPlaceHolder placeHolder)
    {
        if (placeHolder.Controls.Count > 0)
             return true;

        return placeHolder.Controls.IsReadOnly;
    }
}

And then check this in the master page:

Site.Master

<asp:ContentPlaceHolder ID="Content1" runat="server" />
<asp:ContentPlaceHolder ID="Content2" runat="server" />
<asp:ContentPlaceHolder ID="Content3" runat="server" />

<div>Content1: <%= Content1.Controls.Count %></div>
<div>Content2: <%= Content2.Controls.Count %></div>
<div>Content3: <%= Content3.Controls.Count %></div>

<div>Content1 (Ex. Meth.): <%= Content1.ContainsControlsOrCodeBlock() %></div>
<div>Content2 (Ex. Meth.): <%= Content2.ContainsControlsOrCodeBlock() %></div>
<div>Content3 (Ex. Meth.): <%= Content3.ContainsControlsOrCodeBlock() %></div>

As proof-of-concept, I then added a content page:

Index.aspx

<asp:Content ContentPlaceHolderID="Content1" runat="server">
Plain Text Content
</asp:Content>

<asp:Content ContentPlaceHolderID="Content2" runat="server">
<%= "Code block content" %>
</asp:Content>

And all rendered as expected (I believe)..

TBH, while it is not perfect.. I don't think we can get much more elegance in this situation. I am not sure how other control collections are set up in these different scenarios, so I only bolted on to the ContentPlaceHolder control.. Other templated controls may or may not work the same.

Thoughts?

You can download the project from here:

http://code.google.com/p/robcthegeek/source/browse/#svn/trunk/stackoverflow/964724

Rob Cooper
Cheers (+1 and acc) that works a treat. I made one tweak - an additional check for a single LiteralControl containing only whitespace.
Keith
Good call, I was thinking of adding that myself :) Thank you!
Rob Cooper
A: 

Too much for a comment, here's the full code that I finally got working (adapted from @Rob Cooper's answer):

public static bool HasContent( this ContentPlaceHolder placeHolder )
{
 if ( placeHolder.Controls.Count > 0 )
 {
  LiteralControl textBlock;
  ContentPlaceHolder subContent;

  foreach ( var ctrl in placeHolder.Controls )
   if ( (textBlock = ctrl as LiteralControl) != null )
   { //lit ctrls will hold any blocks of text
    if ( textBlock.Text != null && textBlock.Text.Trim() != "" )
     return true;
   }
   else if ( (subContent = ctrl as ContentPlaceHolder) != null )
   { //sub content controls should call this recursively
    if ( subContent.HasContent() )
     return true;
   }
   else return true; //any other control counts as content

  //controls found, but all are empty
  return false;
 }

 //if any code blocks are used the render mode will be different and no controls will
 //be in the collection, however it will be read only
 return placeHolder.Controls.IsReadOnly;
}

This includes two extra checks - firstly for empty literal controls (which occur if the page includes the <asp:Content tags with any whitespace between them) and then for sub-ContentPlaceHolder which will occur for any nested master pages.

Keith