Templated User Controls in ASP.NET
Good design repeats itself. It works hard to convey a whole, a feeling of consistency. Once you understand a part of such a design, you know your way around all of it. This is often done by repetition, using the same elements, colors, styles, positioning, and so on. This is a good thing.
Good code never repeats itself. The number of techniques to avoid it are numerous, and all new languages compete in trying to remove as much repetition as possible (Especially the dynamic ones).
Good design repeats itself, good code does not.
With interface development, you face the conflict above over and over again. You get a design that (rightly) reuses the same concepts over and over, and you need to implement them in code that makes you write the same logic only once. This same time both when writing the code and later when fixing bugs in it, and deep inside, all programmers know that it's the correct way to do things.
I'm currently working in a .NET project (EPiServer CMS 5), and is faced with a design that uses the same kind of boxes all over the site. The boxes only differ by color and content, so things like shadows and rounded corners are clear repetition that I want to do only once. I'll do the shadows and corners with CSS, but for that I need a couple of wrapper divs. Divs that I only want to specify once, and then reuse.
The prequisites are:
- I want a flexible solution, so I'm not tied to a specific HTML structure (number of divs, or even if I use the div tag or not).
- No HTML in properties that get sent to user-controls
- No HTML in code-behind (a common way in .NET to split logic (code-behind) and templates (ASP.NET and HTML))
What I came up with was templated user controls. They provide a way to write controls that wrap any other controls you may have, and add content around them. And it's easy to write and user. This is how the one I wrote is used:
<MyProject:Box runat="server">
<Contents>
<h2>Random header...</h2>
<asp:Repeater runat="server">...</asp:Repeater>
...
</Contents>
</MyProject:Box>
It simply wraps anything inside it (in this case a heading tag and a asp repeater), and lets me do whatever I want with them. In my case, I wanted to add some generic HTML around lots of different content, but you could do anything you wanted.
This is how the above was implemented. First the code-behind:
using System.Web.UI;
namespace MyProject.templates.units
{
[ParseChildren(true)]
public partial class Box : System.Web.UI.UserControl
{
private ITemplate contents = null;
[TemplateContainer(typeof(TemplateControl))]
[PersistenceMode(PersistenceMode.InnerProperty)]
[TemplateInstance(TemplateInstance.Single)]
public ITemplate Contents
{
get
{
return contents;
}
set
{
contents = value;
}
}
void Page_Init()
{
if (contents != null)
contents.InstantiateIn(PlaceHolder1);
}
}
}
… and then the "code-front":
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="Box.ascx.cs" Inherits="MyProject.templates.units.Box" %>
<div class="box">
<div class="boxwrapper">
<asp:Placeholder runat="server" ID="PlaceHolder1" />
</div>
</div>
I think this is a really useful way to write user controls, especially for those of you that work as interface developers in a .NET world. Asking the people around me I found that quite a few didn't know how templated user controls worked, so I hope I will be of use to some of you out there. Happy coding!
Comments
By: Jonatan Larsson (#1)
By: Steve Celius (#2)
Your example is much cleaner, the surrounding markup can always be changed without having to recompile (when IE8 comes out for an example :-)
/Steve
By: Anders (#3)
By: Emil Stenström (#4)
@Steve: Ah, interesting, I've never even heard of that trick. One more thing, did you find the article because it had "EPiServer" in it? You work with them right?
@Anders: Well, not yet. I'm still struggling to get customers to not build for IE6, rounded corners with CSS is far, far, far away. (I'll tell you when I switch, I promise :)
By: Mårten Berg (#5)
...
and in code-behind:
Repeater myNestedRepeater = (Repeater)MyBox.FindControl("PlaceHolder1").Controls[0];
Hope it helps somebody.
By: Emil Stenström (#6)
By: Ken Platt (#7)
By: Emil Stenström (#8)