Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / XHTML

An alternative way of templating controls in ASP.NET

4.67/5 (4 votes)
5 Jul 2012CPOL4 min read 31.8K   276  
Using a custom base template control to manipulate control rendering.

CustomReplaceDemo

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.

C#
// Collection of custom objects that will be
// used to replace variables with actual values.
private List<object> _ReplaceObjects = new List<object>();

// Collection of key values that will be
// used to replace variables with actual values.
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.

C#
protected override void Render(HtmlTextWriter writer)
{
    // Check if any replace objects have been added
    if (ReplaceObjects.Count > 0 || _ReplaceStrings.Count > 0)
    {
        //
        // Additional code, see bellow...
        //
    }
    else
    {
        // Normal rendering
        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.

C#
StringBuilder sb = new StringBuilder();
using (StringWriter sw = new StringWriter(sb))
{
    using (HtmlTextWriter tw = new HtmlTextWriter(sw))
    {
        // Render HTML to stringbuilder
        base.Render(tw);

        // String.Replace method should perform faster than
        // using StringBuilder.Replace.
        string html = sb.ToString();

        //
        // Additional code, see bellow...
        //

        // Write replaced HTML to output stream
        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.

C#
for (int i = 0; i < ReplaceObjects.Count; i++)
{
    // Loop trough all object's properties
    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.

C#
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 nulls for each control on the template is not required and saves us a lot of time.

C#
public static T FindControl<T>(this Control control, string id) where T : Control
{
    // Use normal FindControl method to get the control
    Control _control = control.FindControl(id);

    // If control was found and is of the correct type we return it
    if (_control != null && _control is T)
    {
        return (T)_control;
    }

    // Use reflection to create a new instance of the 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.

ASP.NET
<%@ 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:

HTML
<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.

C#
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;

        // Load template
        CustomReplaceBase articleTemplate = (CustomReplaceBase)
           LoadControl("Templates/ArticleTemplate.ascx");

        // Add custom replace object
        articleTemplate.ReplaceObjects.Add(article);

        // Add custom replace string
        articleTemplate.ReplaceStrings["Number"] = _Count++.ToString();

        // Add template to list item
        e.Item.Controls.Add(articleTemplate);

        // Bind template controls...
        // No logic is implemented on the template itself.
        Button bDetils = articleTemplate.FindControl<Button>("bDetils");

        // You can now add event listeners or manipulate
        // controls on the temaplte...
        // bDetils.Text = "My new text";
        // bDetils.Click += new EventHandler(bDetils_Click);
    }
} 

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.

C++
// Subscribe to the postback forwarding event
articleTemplate.OnPostback += new EventHandler(articleTemplate_OnPostback);
C++
// Postback, outoevent wireup
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... 

 

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)