.Net Travel Notes

Asp.net 2.0 templates system

Introduction

While working on the project SiteBuilder for Windows I was assigned a task to create web template system for Asp.Net sites. Have you ever tried to find a solution like Smarty, Liquid Ruby template engine “but for Asp.Net, please”? I did. And found a few good articles about Application Themes. This approach to customize your site is really a great technique; however this is not an html template engine. One cannot change layout of the whole site using them. There are recommendations to use CSS layout and change it via application themes “for the ultimate power and flexibility”. However, for some reasons I didn’t want to have a lot of cross-browser CSS problems, to learn hacks to display it correctly in Firefox and make them friendly with tweaks for IE. I only needed an html template engine that should be easy to use for both the developer and the designer. I googled a lot, I went through MSDN – nothing handy. So, it was a time to dig myself a little. Below are some results from this solution that you can use in own project.

Engine main classes

As I said, Asp.net 2.0 technology suggested handy method of group substitution of the control properties: Application Themes. And it would be really nice if one could change the page layout using them. However, the System.Web.UI.Page class don’t support for this feature. But the Login control has something very close: it has a default layout, which can be replaced with a custom html layout using the LayoutTemplate property. Since this property can be overridden in each Application Theme, any site theme can have its own html layout for any login form.

NOTE

Its worth to note: this layout must have controls, but the types of these controls can vary. They only have to implement some declated interface. For example, an input field isn’t necessary required to be the TextBox asp.net control. One can use any other control, which provides the IEditableTextControl interface. Moreover, some controls are not required at all: a Login control layout could miss the checkbox RememberMe.

Well, that’s quite interesting. We only need to think a bit more abstract and find a way to build a page that declares what controls it expects from its layout.

At first, let’s define the class Container, which contains “find control in a layout by its id and type” methods.

public class Container : WebControl, INamingContainer
{
public Container()
{}

private ControlType FindControl<ControlType>(class="KEYWORD">string id, bool required, class="KEYWORD">params Type[] additionalTypeRestrictions)
where ControlType : class
{
Control control = this.FindControl(id);
ControlType ret = control as ControlType;
if (ret != null)
{
if (additionalTypeRestrictions == null || additionalTypeRestrictions.Length == 0)
return ret;
bool missedType = false;
Type returnType = ret.GetType();
foreach (Type type in additionalTypeRestrictions)
{
if (!type.IsAssignableFrom(returnType))
{
missedType = true;
break;
}
}
if (!missedType)
return ret;
}

if (required)
{
string types = typeof(ControlType).FullName;
if (additionalTypeRestrictions != null && additionalTypeRestrictions.Length > 0)
{
StringBuilder sb = new StringBuilder(types);
foreach (Type type in additionalTypeRestrictions)
{
sb.AppendFormat(", {0}", type.FullName);
}
types += sb.ToString();
}
throw new InvalidOperationException(
string.Format("Template
is broken: missed required control with id {0} and type(s): {1}."
, id, types));
}

return null;
}

public ControlType FindRequiredControl<ControlType>(class="KEYWORD">string id, params Type[] additionalTypeRestrictions)
where ControlType : class
{
return FindControl<ControlType>(id, class="KEYWORD">true, additionalTypeRestrictions);
}

public ControlType FindOptionalControl<ControlType>(class="KEYWORD">string id, params Type[] additionalTypeRestrictions)
where ControlType : class
{
return FindControl<ControlType>(id, class="KEYWORD">false, additionalTypeRestrictions);
}
}

Now let’s define a control which accepts a layout that can be overriden in the applicationtheme:

public class PagePanel : CompositeControl
{
private ITemplate _Template;
[PersistenceMode(PersistenceMode.InnerProperty)]
[Browsable(false)]
[TemplateContainer(typeof(PagePanel))]
public ITemplate Template
{
get { return _Template; }
set
{
_Template = value;
this.ChildControlsCreated = false;
}
}

private Container _TemplateContainer;
public Container TemplateContainer
{
get
{
EnsureChildControls();
return _TemplateContainer;
}
}

private TemplateWrapper _renderingWrapper;
public TemplateWrapper RenderingWrapper
{
get
{
EnsureChildControls();
return _renderingWrapper;
}
}

public ControlType FindRequiredControl<ControlType>(class="KEYWORD">string id, params Type[] additionalTypeRestrictions)
where ControlType : class
{
return TemplateContainer.FindRequiredControl<ControlType>(id, additionalTypeRestrictions);
}

public ControlType FindOptionalControl<ControlType>(class="KEYWORD">string id, params Type[] additionalTypeRestrictions)
where ControlType : class
{
return TemplateContainer.FindOptionalControl<ControlType>(id, additionalTypeRestrictions);
}

protected override class="KEYWORD">void CreateChildControls()
{
this.Controls.Clear();
if (Template == null)
throw new InvalidOperationException(class="STRING">"Missed template definition.");
_TemplateContainer = new Container();
Template.InstantiateIn(_TemplateContainer);
_renderingWrapper = new TemplateWrapper(_TemplateContainer);
_renderingWrapper.ID = "RenderingWrapper";
this.Controls.Add(_renderingWrapper);
}

public override class="KEYWORD">void RenderBeginTag(HtmlTextWriter writer)
{ }

public override class="KEYWORD">void RenderEndTag(HtmlTextWriter writer)
{ }
}

This class accepts any html layout and provides methods to find a control insideit – these methods are FindOptionalControl and FindRequiredControl. The class TemplateWrapperwhich is used here is just an inheritor of the asp.net control Panel. PagePaneloutput gets wrapped by TemplateWrapper to obtain a few bonuses: default button onthe page, ability to restrict content dimensions and so on. This control has nocritical behavior, however for article completeness there is its code below:

public class TemplateWrapper : Panel
{
Container _container;

public TemplateWrapper(Container container)
{
_container = container;
Controls.Add(container);
}

public override System.Web.UI.Control FindControl(class="KEYWORD">string id)
{
return _container.FindControl(id);
}
}

Probably you’ve already understood how to use the PagePanel control and what advantagesare behind it. But if not – no problem, next chapter explains how to cook and serveit.

Using the new engine

Now let’s see it at work. Imagine we have a simple aspx page that can say helloto the user:

<asp:Content ID="ContentStep" runat="server" ContentPlaceHolderID=class="STRING">"SomeContentID">
<h1>Test page</h1>

<asp:Label ID="WelcomeMessage" runat="server"/><br/>

<asp:TextBox ID="Name" runat="server"/> <asp:RequiredFieldValidator Display=class="STRING">"Static" ID="NameIsRequired" runat=class="STRING">"server" ControlToValidate="Name" ValidationGroup=class="STRING">"SampleForm" ErrorMessage="*" />
<asp:Button ID="SayHello" Text=
"Say Hello"
ForeColor="Green" runat="server" ValidationGroup=class="STRING">"SampleForm" />
</asp:Content>

When the user presses the button SayHello, label WelcomeMessage will display hello message with entered name. Code-behind of this page is very simple, let’s just skip it. Much more interesting is to see how this page can be changed to support variable layout:

<asp:Content ID="ContentStep" runat="server" ContentPlaceHolderID=class="STRING">"SomeContentID">
<SiteBuilder:PagePanel ID="SomePanel" SkinID=class="STRING">"PageName" runat="server">
<Template>
<h1>Test page</h1>

<asp:Label ID="WelcomeMessage" runat="server"/><br/>

<asp:TextBox ID="Name" runat="server"/> <asp:RequiredFieldValidator Display=class="STRING">"Static" ID="NameIsRequired" runat=class="STRING">"server" />
<asp:Button ID="SayHello" ForeColor="Green" runat=class="STRING">"server"/>
</Template>
</SiteBuilder:PagePanel>
</asp:Content>

All page content has been placed into PagePanel control as its template layout. Its SkinID is related to page name, so it already can be changed by application theme.

Code-behind of this new page will be like following:

public partial class PageName : System.Web.UI.Page
{
private ITextControl WelcomeMessage
{
get { return SomePanel.FindRequiredControl<ITextControl>(class="STRING">"WelcomeMessage"); }
}

private IEditableTextControl Name
{
get { return SomePanel.FindRequiredControl<IEditableTextControl>(class="STRING">"Name"); }
}

private IButtonControl SayHello
{
get { return SomePanel.FindRequiredControl<IButtonControl>(class="STRING">"SayHello"); }
}

private BaseValidator NameIsRequired
{
get { return SomePanel.FindRequiredControl<BaseValidator>(class="STRING">"NameIsRequired"); }
}

protected override class="KEYWORD">void OnInit(EventArgs e)
{
base.OnInit(e);
SayHello.Click += new EventHandler(SayHello_Click);
}

protected void Page_Load(class="KEYWORD">object sender, EventArgs e)
{
if (!IsPostBack)
{
string validationGroup = "SampleForm";
NameIsRequired.ControlToValidate = ((Control)Name).ID;
NameIsRequired.ValidationGroup = validationGroup;
NameIsRequired.Text = "*";
SayHello.Text = "Say Hello";
SayHello.ValidationGroup = validationGroup;
}
}

void SayHello_Click(object sender, EventArgs e)
{
if (!Page.IsValid)
return;

WelcomeMessage.Text = string.Format("Hello
{0}!"
, HttpUtility.HtmlEncode(Name.Text));
}
}

Ok, what’s new here:

  • Controls are not computed from page layout any more. They must be declared implicity, its similar to asp.net 1.1. And also they can be optional for any reason now.
  • Page layout is now just a default layout, which can be overriden in application theme using SkinID property of PagePanel.
  • Since page layout will be overriden via application theme (and the application theme should only change visual properites of application) – all page logic constructions must be moved from layout to codebehind or at least out of PagePanel control. This also means that the page controls’ properties must be initialized in Page_Load or OnInit methods.
  • You can decide if any control is mandatory. If it isn’t, you should correctly handle missing control situation.
  • Such realization of PagePanel control doesn’t support Visual Studio Designer for all its power. This means that you have to deal with page layout via Visual Studio text editor.

Let’s take a critical look at these points - inexperienced developers could only have a little trouble with the Visual Designer. If it’s essential (and it really isn’t) – PagePanel class can be updated to support designer too.

So we made some changes to codebehind and what have we been granted? Page layout of this page already can be defined in any application theme. Skin file for this page can look like this (complete example can be found here):

<SiteBuilder:PagePanel SkinID="PageName" runat=class="STRING">"server">
<Template>
<center>
<h4>Test page</h4>

<asp:Label ID="WelcomeMessage" style="font-family:Verdana;
font-size:15px;"
runat="server"/><br/>

<asp:TextBox ID="Name" style="font-family:Verdana;
font-size:15px;"
runat="server"/> <asp:RequiredFieldValidator Display=class="STRING">"Dynamic" ID="NameIsRequired" runat=class="STRING">"server" />
<asp:Button ID="SayHello" ForeColor="Red" runat=class="STRING">"server"/>
</center>
</Template>
</SiteBuilder:PagePanel>

This example shows how the page layout and the visual propeties of the page controls can be overriden. So the developers can only focus on their work and write some application – forum, for example. And then designers come into play and create very different, unique themes for this forum.

That’s all for today. But there are many other things that can be discussed about design templates. For example, how to remove any imperative code from the design template (to separate code and design), but keep the template’s flexibility and moreover add verification for the design template. You’ll find these and other ideas in next notes.

P.S. Thanks to my colleagues and my friends for proof reading and valuable remarks about this note.

Copyright © Alexander Klimov, SWsoft, 2006-2007