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
Most of the ASP.NET 2.0 controls provide ways for the users to customize the look at feel of the output. Some controls such as the Button have relatively little that can be customized while controls such as the DataList or Repeater allow the user nearly complete control over the rendering. In some cases such as a Label this is presented through a series of properties that the user can set and in other cases, like Menu, this is done through templating.
The TreeView offers a rather unique version. The TreeNode object can be extended and used in place of the frameworks TreeNode. In the object, two hooks are exposed that allow the developer to inject their own custom content. These hooks are called "RenderPreText" and "RenderPostText". They are enabled by creating an object which inherits from TreeNode and overriding these virtual functions. Because they are virtual, the base TreeNode class will always call into the function defined in the subclass.
Here's a basic example that is pretty straight forward. A bit of text is emitted from each handler to demonstrate their effect:
| // Basic overriding of the Pre/Post Text methods // This is the simplist form that enables a developer to // add custom content to the rendering of the TreeNode public class CustomTreeNode : TreeNode { protected override void RenderPreText(HtmlTextWriter writer) { writer.Write(" PRE TEXT "); base.RenderPreText(writer); }
protected override void RenderPostText(HtmlTextWriter writer) { writer.Write(" POST TEXT "); base.RenderPostText(writer); }
} | <asp:TreeView ID="TreeView1" runat="server"> <Nodes> <My:CustomTreeNode Text="Node A" Value="Node A"> <My:CustomTreeNode Text="Node B" Value="Node B"> </My:CustomTreeNode> </My:CustomTreeNode> </Nodes> </asp:TreeView>
|
You might imagine that there is a lot that can be accomplished through these methods. Here are a couple examples I put together to demonstrate some various things that can be done:
Setting a per-node background image:
// BG images for TreeNodes // This version adds two features. // 1) it adds a specific property whose value is consumed // 2) it injects an additional control (<div>) into the rendering public class BGTreeNode : TreeNode {
// This constructors is needed if a custom TreeView // instantiates this in CreateNode public BGTreeNode() : base() { } public BGTreeNode(TreeView owner, bool isRoot) : base(owner, isRoot) { }
private string _bgImageUrl; public string BackGroundImageUrl { get { return _bgImageUrl; } set { _bgImageUrl = value; } }
protected override void RenderPreText(HtmlTextWriter writer) { writer.AddStyleAttribute( HtmlTextWriterStyle.BackgroundImage, "url('" + BackGroundImageUrl + "')"); //writer.AddStyleAttribute( // HtmlTextWriterStyle.Height, "35px"); //writer.AddStyleAttribute( // HtmlTextWriterStyle.Width, "300px"); writer.AddStyleAttribute( HtmlTextWriterStyle.TextAlign, "center"); writer.RenderBeginTag( HtmlTextWriterTag.Div ); base.RenderPreText(writer); }
protected override void RenderPostText(HtmlTextWriter writer) { writer.RenderEndTag(); base.RenderPostText(writer); }
} | <asp:TreeView ID="TreeView2" runat="server"> <Nodes> <My:BGTreeNode Text="Node A" Value="A" BackgroundImageUrl="bg2.jpg" > <My:BGTreeNode Text="Node B" Value="B" BackgroundImageUrl="bg3.jpg"> </My:BGTreeNode> </My:BGTreeNode> </Nodes> </asp:TreeView> |
Creating individual TreeNode CSS styles:
| // CssClasses per TreeNode // This is a more general form of the // background image treeNode. This would enable // invidividual customization of TreeNodes public class CSSTreeNode : TreeNode {
// This constructors is needed if a custom TreeView // instantiates this in CreateNode public CSSTreeNode() : base() { } public CSSTreeNode(TreeView owner, bool isRoot) : base(owner, isRoot) { }
private string _cssClass; public string CssClass { get { return _cssClass; } set { _cssClass = value; } }
protected override void RenderPreText(HtmlTextWriter writer) { writer.AddAttribute( HtmlTextWriterAttribute.Class, CssClass); writer.RenderBeginTag(HtmlTextWriterTag.Div); base.RenderPreText(writer); }
protected override void RenderPostText(HtmlTextWriter writer) { writer.RenderEndTag(); base.RenderPostText(writer); }
} | <asp:TreeView ID="TreeView3" runat="server"> <Nodes> <My:CssTreeNode Text="Node A" Value="Node A" CssClass="nodea" > <My:CssTreeNode Text="Node B" Value="Node B" CssClass="nodeb" > </My:CssTreeNode> </My:CssTreeNode> </Nodes> </asp:TreeView>
|
And the ultimate - Templating the PreText/PostText content
Update: One issue I found when using this was that the template needs to be repeatedly assigned. If you need any kind of reuse, there's a follow up technique: http://weblogs.asp.net/dannychen/archive/2006/01/27/436714.aspx
// Templated PreText and PostText of a TreeNode // This enables individual TreeNodes to be able to Template some of their contents. // Done this way, it is a per-node basis meaning that each node that is to be templated // would need the markup for the template it it's declaration. [ParseChildren(false)] public class TemplatedTreeNode : TreeNode {
// This constructors is needed if a custom TreeView // instantiates this in CreateNode public TemplatedTreeNode() : base() { } public TemplatedTreeNode(TreeView owner, bool isRoot) : base(owner, isRoot) { }
private ITemplate _preTextTemplate; [PersistenceMode(PersistenceMode.InnerProperty), TemplateContainer(typeof(TreeNodeTemplateContainer))] public ITemplate PreTextTemplate { get { return _preTextTemplate; } set { _preTextTemplate = value; } }
private ITemplate _postTextTemplate; [PersistenceMode(PersistenceMode.InnerProperty), TemplateContainer(typeof(TreeNodeTemplateContainer))] public ITemplate PostTextTemplate { get { return _postTextTemplate; } set { _postTextTemplate = value; } }
protected override void RenderPreText(HtmlTextWriter writer) { if (PreTextTemplate != null) { TreeNodeTemplateContainer container = new TreeNodeTemplateContainer(this); PreTextTemplate.InstantiateIn(container); container.DataBind(); container.RenderControl(writer); } base.RenderPreText(writer); }
protected override void RenderPostText(HtmlTextWriter writer) { if (PostTextTemplate != null) { TreeNodeTemplateContainer container = new TreeNodeTemplateContainer(this); PostTextTemplate.InstantiateIn(container); container.DataBind(); container.RenderControl(writer); } base.RenderPostText(writer); }
}
// Template container for the TemplatedTreeNode class public class TreeNodeTemplateContainer : WebControl, IDataItemContainer { private TreeNode _node; public TreeNode Node { get { return _node; } }
public TreeNodeTemplateContainer(TreeNode n) { _node = n; }
#region IDataItemContainer Members
public object DataItem { get { return Node; } }
public int DataItemIndex { get { return 0; } }
public int DisplayIndex { get { return 0; } }
#endregion
} | <asp:TreeView ID="TreeView4" runat="server"> <Nodes> <My:TemplatedTreeNode Text="Node A" Value="Node A" > <PreTextTemplate> Text: <%# Databinder.Eval(Container.DataItem, "Text") %> </PreTextTemplate> <PostTextTemplate> Value: <%#DataBinder.Eval(Container.DataItem, "Value")%> </PostTextTemplate> <ChildNodes> <My:TemplatedTreeNode Text="Node B" Value="Node B" > </My:TemplatedTreeNode> </ChildNodes> </My:TemplatedTreeNode> </Nodes> </asp:TreeView> |
There are a couple caveats of doing this. The most obvious one you'll notice is that simplified databinding doesn't work. Simplified databinding needs the controls to exist in the control hierarchy of the page. Since TreeNodes aren't technically controls and don't exist in the control hierarchy of the page, they can't take advantage of simplified databinding. However, as in my example, "old school" databinding still works. Secondly, and less obvious, templates are normally instantiated in CreateChildControls which is much earlier in the lifecycle than Render where these templates are being instantiated. Because of this, there are some quirks such as Databinding events for the templated controls being fired in Render instead of PreRender. Just be aware that these added templates may not act 100% like templates that were designed into a control but they should work pretty well.
Hopefully these examples will give you some ideas of what can be accomplished with Pre/Post Text. One thing you'll quickly figure out when you try this is that you don't have much intellisense for your custom TreeNodes. This is because intellisense is only enabled for objects in the same namespace. Since the TreeView and the TreeNodes are in different namespaces, the intellisense hookup doesn't work. If this is a big issue, you can create a custom TreeView in the same namespace as your custom tree nodes and use that TreeView instead. It's not a necessity.
footnote: if this wasn't enough for you, there's no reason why you couldn't mix your custom node types in the same TreeView either.....
EDIT: Bertrand pointed out below that you would need a custom TreeView to utilize these custom nodes for a DataBound kind of scenario. Heres an example of how that would work:
Custom TreeView Code:
public class MyTreeView : TreeView
{
protected override TreeNode CreateNode()
{
return new CSSTreeNode(this, false);
}
}
Plus Some Markup:
<My:MyTreeView runat="server" ID="MyTreeView1"
DataSourceID="SiteMapDataSource1"
OnTreeNodeDataBound="MyTreeView1_TreeNodeDataBound">
</My:MyTreeView>
<asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server" />
Plus an Event Handler:
<script runat="server">
protected void MyTreeView1_TreeNodeDataBound(object sender, TreeNodeEventArgs e)
{
if (e.Node is CSSTreeNode)
{
CSSTreeNode tn = e.Node as CSSTreeNode;
tn.CssClass = ((SiteMapNode)e.Node.DataItem)["style"];
}
}
</script>
Equals:
I spend a lot of time on the ASP.NET forums. Hopefully my efforts over there help steer users into the right direction of how to use my features and ASP.NET properly and most effectively. But sometimes I see some really alarming things. Recently, the most scary thing I've seen is some quite improperly written SiteMapProviders. Unfortunately, I'm not saying there are just plain buggy or wrongly implemented. Those are the obvious kinds of issues that the developers will find easily through testing and don't really bother me that much. What does bother me is code that will only fail when it's deployed and stressed.
Here's an example, what's wrong with the following code:
' BAD CODE EXAMPLE
Public Class BadProvider
Inherits StaticSiteMapProvider
Private _rootNode As SiteMapNode
Public Overrides Function BuildSiteMap() As SiteMapNode
If _rootNode Is Nothing Then
AddNode(newNode1, Nothing)
AddNode(newNode2, newNode1)
AddNode(newNode3, newNode1)
_rootNode = newNode1
End If
Return _rootNode
End Function
Protected Overrides Function GetRootNodeCore() As SiteMapNode
Return BuildSiteMap()
End Function
Protected Overrides Sub Clear()
_rootNode = Nothing
MyBase.Clear()
End Sub
Protected Sub MyInvalidationMethod()
Clear()
End Sub
End Class
Give up yet? Simply and generally stated, the problem is that it's not Thread Safe Why not? Well, perhaps this is the hardest concept for users to either grasp or accept about the Provider Model. Providers are instantiated as a single instance across the app domain. This means a single provider is shared by all of the worker processes (ie all the requests) that come into the site.

In this example, one particular request triggers the Clear() event while the others all query data. Each query to data implicitly calls into BuildSiteMap out of necessity. There are several potential problems here:
1) A race condition between BuildSiteMap and Clear trying to modify the data at the same time
2) Simultaneous threads executing the BuildSiteMapCode at the same time
3) A race condition between BuildSiteMap, Clear, and the calling page performing future accesses
These are not new issues presented by the SiteMapProvider model, these are fundamental issues that need to be dealt with in any multithreaded design. Ok, I've talked the talk, how do I walk the walk. With ASP.NET, it's relatively easy. Simply use the lock(object) (C#) or SyncLock object (VB) features. The important part is to know what to lock. The StaticSiteMapProvider protects all of it's APIs but anytime an API is overridden that can modify the data, it needs to be protected. This means: AddNode, RemoveNode, Clear, and BuildSiteMap. Here's the previous example properly coded. If AddNode or RemoveNode needs to be overriden, they should be overridden the same way Clear has been.
Public Class GoodProvider Inherits StaticSiteMapProvider
Private lockObj As New Object Private _rootNode As SiteMapNode
Public Overrides Function BuildSiteMap() As SiteMapNode Dim _tempRoot As SiteMapNode = _rootNode
If _tempRoot IsNot Nothing Then Return _tempRoot End If
SyncLock lockObj If _rootNode Is Nothing Then MyBase.Clear() AddNode(newNode1, Nothing) AddNode(newNode2, newNode1) AddNode(newNode3, newNode1)
_rootNode = newNode1 End If Return _rootNode End SyncLock End Function
Protected Overrides Function GetRootNodeCore() As SiteMapNode Return BuildSiteMap() End Function
Protected Overrides Sub Clear() SyncLock lockObj _rootNode = Nothing End SyncLock End Sub
Protected Sub MyInvalidationMethod() Clear() End Sub End Class | public class GoodProvider : StaticSiteMapProvider { private object _lockObj = new object(); private SiteMapNode _rootNode;
public override SiteMapNode BuildSiteMap() { SiteMapNode _tempRoot = _rootNode;
if (_tempRoot != null) return _tempRoot;
lock (_lockObj) { if (_rootNode == null) { base.Clear(); AddNode(newNode1, null); AddNode(newNode2, newNode1); AddNode(newNode3, newNode1);
_rootNode = newNode1; } return _rootNode; } }
protected override SiteMapNode GetRootNodeCore() { return BuildSiteMap(); }
protected override void Clear() { lock (_lockObj) { _rootNode = null; } }
protected void MyInvalidationFunction() { Clear(); }
} |
One thing to look more closely at in this example is the BuildSiteMap method. As I previously mentioned, all the data reading APIs of the SiteMapProvider call into BuildSiteMap to ensure the provider data has been populated. In short, it gets called a lot. Because of this, it's important that the primary flow of that function not be locked and only the creation portion does.
Update 1/12/2006: I was alerted to a couple remaining race conditions in my good code so I've updated BuildSiteMap to eliminate them. Thanks Joe for looking at this.