I 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