Introduction
This article explains using an alternative approach to control templates with the ability to replace certain string values at runtime.
Background
I required some way of using templates that can support both ASP.NET controls and custom HTML replace. Remember the [Key] to "Value" replacement strings from classic ASP? I find it quite useful when dealing with inline JavaScript or CSS style classes, for example. We can also use reflection to automatically loop trough all the properties of an object and replace them on our template.
There were also some other features I needed to implement, so I decided to go for a base class.
Using the code
First, we start off by creating a base class which all our template controls will inherit from.
Remember that your custom base class must inherit at least from the Control
class or the UserControl
class, which depends on what you will use the control for. In our case, we will inherit from a UserControl
since we want to be able to dynamically load our template control at runtime.
The next step is to create some containers for our replace object.
private List<object> _ReplaceObjects = new List<object>();
private NameValueCollection _ReplaceStrings = new NameValueCollection();
We create two members that will hold all our replace objects and strings we want to get replaced. In the case of an object, we will be replacing its public properties' values, and in the case of a string, the string value itself will get replaced. How exactly it works will be discussed later.
The actual magic happens when the control is being rendered, so we have to override the Render
method of our base class.
protected override void Render(HtmlTextWriter writer)
{
if (ReplaceObjects.Count > 0 || _ReplaceStrings.Count > 0)
{
}
else
{
base.Render(writer);
}
}
If there are no replace objects or strings in our collections, we render the content directly to the output stream.
In case there is at least one object or string waiting to be replaced, we render the control to a StringBuilder
instead of directly rendering to the output stream. This gives us the chance to manipulate the rendered control's HTML before sending it to the output stream.
StringBuilder sb = new StringBuilder();
using (StringWriter sw = new StringWriter(sb))
{
using (HtmlTextWriter tw = new HtmlTextWriter(sw))
{
base.Render(tw);
string html = sb.ToString();
writer.Write(html);
}
}
As mentioned in the background part already, if you remember using this technique in ASP, I'm sure you also remember writing a lot of Replace(html, "[key]", "val")
lines to replace public properties for classes. Have no fear, Reflection is here :)
Using Reflection, we can loop trough all the properties and get the value of each one at runtime.
for (int i = 0; i < ReplaceObjects.Count; i++)
{
foreach (PropertyInfo prop in ReplaceObjects[i].GetType().GetProperties())
{
if (ReplaceObjects[i] != null)
{
object val = prop.GetValue(ReplaceObjects[i], null);
if (val == null)
{
html = html.Replace("[" + prop.Name + "]", "");
}
else
{
html = html.Replace("[" + prop.Name + "]", val.ToString());
}
}
}
}
Fairly simple. We loop trough our replace objects collection, and using Reflection, we get a collection of properties that the object exposes. The property name is used as the key that will be replaced on the template with the property value. You could extend this sample with a recursive call that would also loop through all sub objects; keep performance penalty in mind.
Replacing our collection of custom strings is even easier. A simple loop with a call to the Replace
method will do the trick.
if (_ReplaceStrings.Count > 0)
{
for (int i = 0; i < _ReplaceStrings.Count; i++)
{
html = html.Replace("[" + _ReplaceStrings.Keys[i] +
"]", _ReplaceStrings[i]);
}
}
Handling server controls
We can handle server controls using the FindControl
method. Because some templates can include a certain server control and some don't, I have implemented a custom variant of the FindControl
method, which instead of returning null
when the control is not found, returns a new instance of the control. Manipulating instances of these "null" controls does not affect the rendered content since they do not exist on the page. This way, checking null
s for each control on the template is not required and saves us a lot of time.
public static T FindControl<T>(this Control control, string id) where T : Control
{
Control _control = control.FindControl(id);
if (_control != null && _control is T)
{
return (T)_control;
}
return (T)Activator.CreateInstance(typeof(T));
}
As I found this really useful, I decided to implement it as an extender method so it could be used on all ASP.NET controls that inherit from the Control
base class.
Preparing a template
This is a simple template example which enables you to combine both server controls and replace strings.
<%@ Control Language="C#" AutoEventWireup="true"
Inherits="CustomReplaceDemo.CustomReplaceBase" %>
<h2>
[Title]
</h2>
(ID: [ArticleID])
<br />
Price: [Price] EUR
<asp:Button runat="server" ID="bDetils" Text="Details"
OnClientClick="javascript:alert('[Title] costs [Price] EUR.');return false;" />
Remember that all your templates have to inherit from you template base class.
Quick example
Put a Repeater
control on your ASPX page:
<asp:Repeater ID="rItems" runat="server">
<ItemTemplate>
</ItemTemplate>
</asp:Repeater>
Bind any data you want to the Repeater
control. In this example, it's a basic product class that holds some product data like Article ID, Title, and Price.
When the data is bound, we load the template for each repeater item, just like loading a regular user control.
void rItems_ItemDataBound(object sender,
System.Web.UI.WebControls.RepeaterItemEventArgs e) {
if (e.Item.ItemType == ListItemType.Item ||
e.Item.ItemType == ListItemType.AlternatingItem)
{
Article article = (Article)e.Item.DataItem;
CustomReplaceBase articleTemplate = (CustomReplaceBase)
LoadControl("Templates/ArticleTemplate.ascx");
articleTemplate.ReplaceObjects.Add(article);
articleTemplate.ReplaceStrings["Number"] = _Count++.ToString();
e.Item.Controls.Add(articleTemplate);
Button bDetils = articleTemplate.FindControl<Button>("bDetils");
}
}
Postback handling
The postback control must subscribe to the Postback event handler in the base template, which will forward the request to the code bellow.
<asp:Button runat="server" ID="bPostback" Text="Postback" OnClick="Postback" />
There are two ways to subscribe to postback events on a template. First
of we can subscribe to the OnPostback event on the template or secondly
implement a custom base class for the page that hosts the template.
articleTemplate.OnPostback += new EventHandler(articleTemplate_OnPostback);
public override void OnPostback(object sender, EventArgs e)
{
...
}
Conclusion
I am already using this approach in a few projects, and they seem to work pretty well. Of course, the release version should look a bit different than the demo project, like disabling the app restart when a template is changed, so you can edit templates on the fly...