ASP.NET: Creating a UserControl with Child Content

square-peg-round-hole-21 by Yoel Ben-AvrahamI love ASP.NET User Controls, aka “ascx” files.  These little guys are great for reusable content and dividing up the components of a website.  The little brother of the more powerful Custom Server Control, they have some limitations, but I’ve found they are often sold short.  Once falsehood is that a User Control cannot contain content between the opening and closing tags, like such:

<MyControls:ContentBlock runat="server" Title="The Control has Content!">
<
p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Donec ut nisi sed elit aliquam vulputate eu et lacus</p>
<
asp:HyperLink runat="server" NavigateUrl="~/">Home</asp:HyperLink>
</
MyControls:ContentBlock>

If you attempt something like this with a stock User Control you’ll get “Type 'ASP.mycontent_contentblock_ascx' does not have a public property named 'p'.”  If we just want to have a simple control that formats the content, then we only need a few tweaks and we can keep the User Control (if you have more advanced needs, then it’s time to step up to a Custom Server Control).

The first tweak is to wrap the ascx template in a single control:

<%@ Control Language="C#" AutoEventWireup="true" 
CodeFile="ContentBlock.ascx.cs" Inherits="MyControl_ContentBlock" %>
<asp:PlaceHolder ID="phContent" runat="server">
<
h1>
<%= Title %>
</h1>
<
div class="body">
<%= Text %>
</div>
</
asp:PlaceHolder>

I choose a PlaceHolder control because it won’t emit any HTML.  Then in code-behind we need to make a few tweaks:

[ParseChildren(false)]
public partial class MyContent_ContentBlock : System.Web.UI.UserControl
{
public String Title { get; set; }
protected String Text { get; set; }

protected override void AddParsedSubObject(object obj) {
if (obj is LiteralControl)
this.Text += ((LiteralControl)obj).Text;

else if (obj is PlaceHolder && !String.IsNullOrEmpty(((PlaceHolder)obj).ID)
&& ((PlaceHolder)obj).ID.Equals("phContent"))
base.AddParsedSubObject(obj);

else {
StringBuilder sb = new StringBuilder();
using (StringWriter sw = new StringWriter(sb)) {
using (HtmlTextWriter w = new HtmlTextWriter(sw)) {
((Control)obj).RenderControl(w);
this.Text += sb.ToString();
}
}
}
}
}

The class gets a new attribute, ParseChildren, set to false.  I’m stealing this attribute from the Custom Server Control which normally would use this attribute to map the child content to a property.  Setting it to false however on a User Control will stop the framework from throwing an error on the User Control.  If you stop here, the output of the control would look something like:

<h1>
The Control has Content!
</h1>
<
div class="body">
</
div>
<
p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Donec ut nisi sed elit aliquam vulputate eu et lacus</p>
<
a href="/Default.aspx">Home</a>

The content was appended to the User Control template.  This can be fixed by overriding the AddParsedSubObject method.  This method is called for every control in the User Control – including the PlaceHolder that acts as our template.  Any HTML content is converted into a LiteralControl.  The logic checks the control being passed in, and if it’s a LiteralControl the text is added to the control’s Text property.  If the control is the PlaceHolder it is passed to the base method to let normal processing takeover.  The remaining controls are rendered, and the text appended to the control’s Text property.  Technically I don’t need the special case for LiteralControls, since the last case will handle it, but it’s the common case and worth saving the additional CPU cycles required to use the RenderControl method.

We now have the proper output:

<h1>
The Control has Content!
</h1>
<
div class="body">
<
p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Donec ut nisi sed elit aliquam vulputate eu et lacus</p>
<
a href="/Default.aspx">Home</a>
</
div>

There is a gotcha with this approach (beyond advanced data binding and postback issues you may encounter) – the following code will botch the output:

<MyControls:ContentBlock runat="server">
<%= DateTime.Now %>
</MyControls:ContentBlock>

Simply put, Code Blocks change the way controls get processed.  Wrapping Code Blocks in another server control will work around this problem:

<MyControls:ContentBlock runat="server">
<
asp:PlaceHolder runat="server"><%= DateTime.Now %></asp:PlaceHolder>
</
MyControls:ContentBlock>

Now that you have this new power, promise me you’ll only use it where proper and create a Custom Server Control when needed.

Promise?


Posted 12-07-2009 9:10 PM by Michael C. Neel
Filed under:

[Advertisement]

Comments

Moutasem al-awa wrote re: ASP.NET: Creating a UserControl with Child Content
on 12-08-2009 3:52 AM

I promise :), it is a really useful one and i will try it with SharePoint MasterPages and get back to you for any new updates or bugs.

Thanks again

khaled wrote re: ASP.NET: Creating a UserControl with Child Content
on 12-09-2009 5:42 PM

Good Idea, Thanx alot

johnpearson wrote re: ASP.NET: Creating a UserControl with Child Content
on 12-30-2009 2:19 PM

What about when one of the child controls is a button.

Error: Control 'btnHidePopup_LinkButton1' of type 'LinkButton' must be placed inside a form tag with runat=server.

Eventhough the page that has the user control also has a form, the above error happens.

Michael C. Neel wrote re: ASP.NET: Creating a UserControl with Child Content
on 01-02-2010 1:41 PM

I don't have any errors with using a Button, but as I said this isn't going to work in all situations and you'll probably need a custom control if you need to put user input and buttons inside the template content.

About The CodeBetter.Com Blog Network
CodeBetter.Com FAQ

Our Mission

Advertisers should contact Brendan

Subscribe
Google Reader or Homepage

del.icio.us CodeBetter.com Latest Items
Add to My Yahoo!
Subscribe with Bloglines
Subscribe in NewsGator Online
Subscribe with myFeedster
Add to My AOL
Furl CodeBetter.com Latest Items
Subscribe in Rojo

Member Projects
DimeCasts.Net - Derik Whittaker

Friends of Devlicio.us
Red-Gate Tools For SQL and .NET

NDepend

SlickEdit
 
SmartInspect .NET Logging
NGEDIT: ViEmu and Codekana
LiteAccounting.Com
DevExpress
Fixx
NHibernate Profiler
Unfuddle
Balsamiq Mockups
Scrumy
JetBrains - ReSharper
Umbraco
NServiceBus
RavenDb
Web Sequence Diagrams
Ducksboard<-- NEW Friend!

 



Site Copyright © 2007 CodeBetter.Com
Content Copyright Individual Bloggers

 

Community Server (Commercial Edition)