Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Issue with asp:ContentPlaceHolder and code blocks

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?

like image 784
Keith Avatar asked Jun 08 '09 12:06

Keith


3 Answers

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

like image 188
Rob Cooper Avatar answered Nov 17 '22 08:11

Rob Cooper


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.

like image 23
Keith Avatar answered Nov 17 '22 07:11

Keith


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.

like image 2
Chris Van Opstal Avatar answered Nov 17 '22 06:11

Chris Van Opstal