Partager via


Templates - Part 2

In my last article, we talked about writing custom web controls that could render UI based on a template. Controls are able to parse properties between their opening and closing tags. These properties can set various public attributes of the control itself or populate the control’s child control collection. These properties can also be ITemplate attributes, in which case ASP.NET is able to create an instance of a template that our control can use for whatever purpose it wishes.

 

In this part, I’ll be discussing taking this one step further and creating an architecture that can fully abstract control UI using an ASCX file.

 

Many ASP.NET developers have used ASCX files to define web controls that get compiled at run time. These files behave as “mini web pages” and like web pages have HTML code, server side controls, and server side code. These are nice because they provide developers a way to encapsulate their UI in a separate file without having to “hard code” HTML elements within their code or call Controls.Add() a bunch of times. However, how can one create an instance of these ascx controls at runtime? Well, ASP.NET’s page parser has to do it some how, so how does that work? Let’s take a look.

 

Once you know that the Page class is actually derived from Control, it’s easy to see how an entire page is nothing more than a control with its child controls specified in an ASPX file, and a web control is the same thing with a different extension. When the page parser comes across a reference to another control, and the reference for that control points to an ascx file, the file is opened and parsed using the Page.LoadControl method. LoadControl has an overload to specify a file name to load the control from. This filename can be (well, should be) an ascx file. LoadControl returns an instance to that control.

 

So where can this come in handy? Well, let’s take a look at a few examples. The first is this application I’m using right now to post my blog – Community Server 2.0. This software allows blog administrators to upload files to customize the look and feel of their blog site. These files are in the forms of ascx files, and have references to server side web controls to use used as place holders for various blog content. These controls are used to render menus, links, and blog content itself. Taking a look at some of the sample templates will give you a good idea of how everything works. Now, I’ve never seen the source code for Community Server, but I can imagine they call LoadControl to load these user uploaded ascx files into memory from their base class and populate the page’s child controls.

 

Another example is SharePoint. SharePoint has an entire directory of control templates for their various templated controls. One is DefaultTemplates.ascx which contains a huge list of “RenderingTemplate” instances. The file is loaded in with LoadControl which returns a control collection with all the various templates from the file. Controls.FindControl() can be used to locate the specific template needed.

 

Should you create one big file for all your templates like SharePoint does? I don’t know, it’s up to you. But in my production code, I have about 20 templates in a single file and it takes no detectable time to parse the entire file. Only the constructors of each object would be called and memory allocation is so blazing fast in managed code, it’s probably not a big deal. But if it makes you feel better, you can create a separate file for each template.

 

So let’s build a control that can use an ascx file, shall we?

 

First, we need a control that acts as a container for the template. The template within the container can interact with the outside world through this container class, so anything you template needs access to (such as DataItem) needs to be accessible through this class. You can refer to this instance of this class using the reserved “Container” keyword, such as Container.DataItem. It’s all starting to come together in your head by now, isn’t it?

 

   public class MyTemplateContainer : Control, INamingContainer

   {

      private TestControl _control;

      public TestControl Control

      {

         get { return _control; }

      }

      public MyTemplateContainer(TestControl control) : base()

      {

         _control = control;

      }

   }

 

Our container control is called MyTemplateContainer and only has a single public property called TestControl. We’ll use this as a reference to the parent control that’s rendering the template.

 

Next, we need a control to reference withinin the ASCX file itself. This control represents a single template instance. When we call LoadControl, we create an instance of this control.

 

   [ParseChildren(true)]

   [PersistChildren(false)]

   public sealed class MyTemplate : Control

   {

      private ITemplate _template = null;

      [TemplateContainer(typeof(MyTemplateContainer))]

      public ITemplate Template

      {

         get { return _template; }

         set { _template = value; }

      }

   }

 

This can have any number of properties which you can set declaratively in the ascx file, but our example just has a single property called “Template”. ASP.NET will automatically set this property based on the contents of the ascx file. So what does our ascx file look like? Well how about something like this:

 

<%@ Control Language="C#" %>

<Test:MyTemplate ID="MyControl" runat="server">

    <Template>

      <table>

         <tr><td>

            <ASP:TextBox Text="<%# Container.Control.TestProperty %>" runat="Server" />

         </td></tr>

      </table>

    </Template>

</Test:MyTemplate>

When we load this ascx file into memory, we create a “Control” object that has a single child control called MyControl that is of type MyTemplate. Inside the <Test:MyTemplate> tag, ASP.NET recognizes “Template” as a public property of the MyTemplate class. It parses the HTML inside the <Template> tag and creates an instance of an ITemplate object. Through the [TemplateContainer] attribute it knows that Container will be an instance of “MyTemplateContainer” so it can verify Container.Control.TestProperty is a valid expression. It would of course be possible to set other properties on the MyTemplate object and read them at runtime. You can also just as easily define other templates, such as Header templates, footer templates, item templates, etc.

 

Now what about our control itself? Let’s take a look at how to load this ascx file, read the templates, and use them to populate our control at runtime. Here’s what the control code looks like:

 

   public class TestControl : Control

   {

      public string TestProperty = "Hello World!";

      protected override void OnInit(EventArgs e)

      {

         Control control = Page.LoadControl("ControlTemplate.ascx");

         if (control != null)

         {

            MyTemplate t = control.FindControl("MyControl") as MyTemplate;

            if (t.Template != null)

            {

               Controls.Clear(); //Clear any existing controls

               MyTemplateContainer TemplateContainer = new MyTemplateContainer(this);

               t.Template.InstantiateIn(TemplateContainer);

               Controls.Add(TemplateContainer);

            }

         }

         base.OnInit(e);

      }

   }

 

Let’s step through this. First, we have a control called TestControl derived from Control. We can place this control on any page we want, or new up an instance of this control at runtime. This control can even be a Web Part that lives in a serialized state in a database some where. It doesn’t really matter since we load the template we need at runtime.

 

During OnInit, we load our template file into memory and populate our child controls. Let’s step through this:

 

1) We call Page.LoadControl() to load in our ascx file (defined above.)

2) LoadControl returns a single control that contains all the controls in our file. We know we only have one and we know it’s called MyControl. We know it’s of type MyTemplate so we can use FindControl to locate it.

3) If we found such a control (nobody’s been mucking with our file) and it has a valid Template property, we create a new template container.

4) Through the ITemplate interface, we instantiate our template within the template container.

5) At this point, TemplateContainer will have a populated Controls hierarchy with everything in our template.

6) Now, we simply add the TemplateContainer instance to our control’s child control collection.

 

Easy enough? Now we have a control that can dynamically load a file on the file system at runtime to populate its UI. This is how SharePoint allows administrators to customize web part and list UI, and this is how applications such as Community Server allow administrators to customize its UI. This allows controls that may appear on dozens of web pages on a single site to all share a common UI template that isn’t “hard coded” into compiled code. Pretty cool, eh?

 

One thing I thought I’d mention that I ran into when I was experimenting with this stuff initially. The code that builds an ITemplate instance from your <Template> property is not quite as advanced as the default page parser in ASP.NET. It seems you can not do stuff like bind event handlers to server side controls. For example, I cannot do this:

 

    <Template>

      <table>

         <tr><td>

            <ASP:TextBox OnChange="<%# Container.Control.HandleChange %>" runat="Server" />

         </td></tr>

      </table>

 

Where HandleChange is a public event that matches the delegate for TextBox’s OnChange event. Event binding is not something this parser supports. However, I would assume it would be possible to write your own “LoadControl” override that can parse the <Template> property and return your own implementation of ITemplate whose IntiantiateIn implementation can bind event handlers. If someone gets that working, I’d love to hear about that.

 

Let me know if you have any questions!

 

Mike