Making the CompiledTemplateBuilder work for you. Create a Template Library.

This is a bit of a follow on to my last post.  At the end, I made an update about how to make use of custom TreeNodes in a databound scenario.  I did that example with the CSSTreeNode instead of the TemplatedTreeNode because the custom property in the templated case was an ITemplate.  Trying to assign a value to that property is a great deal more difficult than a string property.  And that got me thinking...

Lets start with something that seems pretty simple: 

<asp:SiteMapPath ID="SiteMapPath1" runat="server">
    <NodeTemplate>
        <%# Eval("title") %>
    
</NodeTemplate>
</asp:SiteMapPath>

What if you wanted to add this control to the page dynamically?  How would you write it in code?  Well, adding the SiteMapPath to the page is pretty simple.  But the NodeTemplate isn't so simple.  What actually happens in that case is a mechanism in the framework creates a new object on the fly that implements the ITemplate interface and generates some code to populate the template.  In order to do this manually, the developer would have to create his/her own ITemplate object and databinding code.  Here's a simplified version of what that code might look like:

public class MyTemplate : ITemplate
{
    
public void InstantiateIn(Control container)
    {
        
Label l1 = new Label();
        container.Controls.Add(l1);
        l1.DataBinding +=
new EventHandler(l1_DataBinding);    
    }

    
void l1_DataBinding(object sender, EventArgs e)
    {
        
Label l1 = sender as Label;
        
SiteMapNodeItem container = l1.BindingContainer as SiteMapNodeItem;
        l1.Text = container.SiteMapNode.Title;
    }
}
 
public partial class TestPage : System.Web.UI.Page
{
    
protected void Page_Load(object sender, EventArgs e)
    {
        
SiteMapPath smp = new SiteMapPath();
        smp.NodeTemplate =
new MyTemplate();
        form1.Controls.Add(smp);
    }
}

Keep in mind, this code is for a single eval statement.  If you wanted to add several controls, some styles, maybe another template or two (perhaps 20 lines of markup), this could quickly become 100+ lines of code for this single control.  This is especially unfortunate because this is work that the parser can do reliably and quickly. 

So the question is, how can I get the parser to parse the template markup for me and yet present me with a reference to the ITemplate object instead of consuming it?
  

  

The Template Library Control

What we need is a control (with no rendering, kind of like a datasource) with a public property that is a collection of templates.  Then similarly to how the page parser will populate a ListItem collection or a MenuItem collection for you, it could populate a Template collection.  It'd be additionally nice to have random access to the list through a Name rather than by index.  Amazingly enough, this actually works: (link to full listing at the bottom of the article):

    
    [
ParseChildren(true,"Templates"), PersistChildren(false)]
    
public class TemplateLibrary : Control
    {
        
public TemplateLibrary()
        {
            _templates =
new TemplateList();
        }
        
        
private TemplateList _templates;
        [
PersistenceMode(PersistenceMode.InnerProperty)]
        
public TemplateList Templates
        {
            
get return _templates; }
        }

    }

 
   public class TemplateList : List<TemplateItem>
    {
        
public TemplateItem this[string key] { ... }
    }

    public class TemplateItem : ITemplate
    {
        
private string _name;
        
private ITemplate _template;

        
public string Name { ... }

        [PersistenceMode(PersistenceMode.InnerDefaultProperty),
        
TemplateContainer(typeof(IDataItemContainer))]        
        
public ITemplate Template { ... }

        #region ITemplate Members
        
public void InstantiateIn(Control container) { ... }
        #endregion
    }
}

Here's how this might be used in markup:

<MS:TemplateLibrary  runat="server" ID="TemplateLibrary1">
    <MS:TemplateItem Name="Template1">
        <Template>Some Content</Template>
    </MS:TemplateItem>            
    
<MS:TemplateItem Name="Template2">
        <Template>Some Other Content</Template>
    </MS:TemplateItem>            
</MS:TemplateLibrary>
 
 
protected void Page_Load(object sender, EventArgs e)
{
    Menu1.StaticItemTemplate = TemplateLibrary1.Templates[
"Template1"];
    Menu1.DynamicItemTemplate = TemplateLibrary1.Templates[
"Template2"];
}
 

As shown here, the end result is that the templates are built by the parser but exposed through a public property on the control.  The control itself has no inherent rendering so it doesn't really affect the page output.  Since the templates are assigned in code, some special logic could be used to choose which template should be applied where.  Here are a couple more examples of things you can do that would, otherwise, have been a great deal more difficult to accomplish. 

Example 1: This first example makes uses of the templated TreeNodes from the previous post.  In this case, I'm choosing, dynamically, based on the data in web.sitemap, which template should be applied.

<%@ Page Language="C#" Debug="true" %>
<%
@ Register Namespace="MSSamples" TagPrefix="MS" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<
script runat="server">
    
    
protected void MyTreeView1_TreeNodeDataBound(object sender, TreeNodeEventArgs e)
    {
        
if (e.Node is TemplatedTreeNode)
        {
            
TemplatedTreeNode tn = e.Node as TemplatedTreeNode;
            
SiteMapNode smn = e.Node.DataItem as SiteMapNode;
                        
            
if( smn != null && smn["preTemplate"] != null )
                tn.PreTextTemplate = TemplateLibrary1.Templates[ smn[
"preTemplate"] ];
            
            
if( smn != null && smn["postTemplate"] != null )
                tn.PostTextTemplate = TemplateLibrary1.Templates[ smn[
"postTemplate"] ];
        }
    }

</script>

<
html xmlns="http://www.w3.org/1999/xhtml">
<
head id="Head1" runat="server">
    <title>Untitled Page</title>
</
head>
<
body>
    <form id="form1" runat="server">
        <div>
            <MS:MyTreeView runat="server" ID="MyTreeView1" ExpandDepth="2" DataSourceID="SiteMapDataSource1"
                OnTreeNodeDataBound="MyTreeView1_TreeNodeDataBound">
            </MS:MyTreeView>
            <asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server" />
                        
            
<MS:TemplateLibrary runat="server" ID="TemplateLibrary1">
                <MS:TemplateItem Name="arrow">
                    <Template>
                        ==>                        
                    
</Template>
                </MS:TemplateItem>
                <MS:TemplateItem Name="valuePath">
                    <Template>
                        ValuePath:
                        <%
# DataBinder.Eval(Container.DataItem, "ValuePath") %>
                    
</Template>
                </MS:TemplateItem>
            </MS:TemplateLibrary>
        </div>
    </form>
</
body>
</
html>
 
 
Web.SiteMap Contents:
 
<?xml version="1.0" encoding="utf-8" ?>
<
siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
  <
siteMapNode title="Home" preTemplate="arrow">
    <
siteMapNode title="Products" >
      <
siteMapNode title="Hardware" postTemplate="valuePath"/>
    </
siteMapNode>
  </
siteMapNode>    
</
siteMap>
 
Output:

Example 2: The key concept in this example is that the template library could be shared through a user control to every page that requires it. 

 
Templates.ascx:
<%@ Control Language="C#" ClassName="Templates" %>
<%
@ Register Namespace="MSSamples" TagPrefix="MS" %>

<script runat="server">
    public TemplateLibrary TLib
    {
        
get { return TemplateLibrary1; }
    }
</script>

        <MS:TemplateLibrary  runat="server" ID="TemplateLibrary1">
            <MS:TemplateItem Name="greenTemplate">
                <Template>
                    <div style="background-color:Green"><%# Eval("Text") %></div>
                </Template>
            </MS:TemplateItem>            
            
<MS:TemplateItem Name="yellowTemplate">
                <Template>
                    <div style="background-color:Yellow"><%# Eval("Text") %></div>
                </Template>
            </MS:TemplateItem>            
            
<MS:TemplateItem Name="redTemplate">
                <Template>
                    <div style="background-color:Red"><%# Eval("Text") %></div>
                </Template>
            </MS:TemplateItem>            
        
</MS:TemplateLibrary>
 
 
MyPage.aspx
 
<%@ Page Language="C#" %>
<%
@ Import Namespace="MSSamples" %>
<%
@ Register Src="Templates.ascx" TagName="Templates" TagPrefix="uc1" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<
script runat="server">

    protected void Page_Load(object sender, EventArgs e)
    {
        
foreach( TemplateItem ti in Templates1.TLib.Templates )
        {
            
Menu m1 = new Menu();
            m1.Items.Add(
new MenuItem("Item One"));
            m1.Items.Add(
new MenuItem("Item Two"));
            m1.StaticItemTemplate = ti;
            form1.Controls.Add(m1);
        }
    }
</script>

<
html xmlns="http://www.w3.org/1999/xhtml" >
<
head runat="server">
    <title>Untitled Page</title>
</
head>
<
body>
    <form id="form1" runat="server">
    <div>
        <uc1:Templates id="Templates1" runat="server">
        </uc1:Templates>
    </div>
    </form>
</
body>
</
html>
 
Output:
 
EDIT: One subtle note that I don't really mention in the article.  A large portion of DataBinding happens through reflection.  This really lets the Templates in the "library" maintain their ability to databind even though the container and data object isn't yet known.  This is cool because if two data objects happen to have the same property name, the same template could be used for either one.  I did assume that the container would be an IDataItemContainer, and that seems to work pretty well. 

Link to C# code listing

5 Comments

  • I've been working on a templated control idea that has child templatres similar to this:


    ==>



    I have been trying to figure out if there is a way to not need the element when my control has only one template.

    Without the element, your arrow template could be very compact:
    ==>

    Do you know if there is a way?


  • This is excellent! Thanks for the post.
    I am having trouble accessing attributes of the sitemapnodes. I want to be able to include the Url in the template. Could you help?

  • I would like to see the inscription to be continied

  • thanks for good content how to make sitemap templetes

  • I like this, but Im a little lost with how I am trying to use this. I would like to programatically control which template should show. Rather than just showing them all. Similar to what the loginview control does.

    For example (pseudo code):

    If condition one
    show template one
    If condition two
    show template two
    else
    show template three

    Assuming that I have defined all templates in the TemplateLibrary class.

Comments have been disabled for this content.