Customizing TreeNodes with RenderPreText and RenderPostText

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:

10 Comments

  • Nice. One problem I see here is that templating in particular will only work with static nodes. To enable these scenarios with dynamically added nodes (databound tree or even populate on demand), you need to derive from TreeView as well and override CreateNode to return the right derived node type.

  • Bertrand, Stefaan,

    Thanks for the feedback and suggestions. I'll update the samples.

    --

    Danny

  • Stefaan,

    Sorry, I don't really know of a good solution for this other than writing a new control designer.

    Honestly, one of my low points is with control designers. I'm not yet familiar with them at all. In the next few weeks, I'll probably spend some time learning more about them and produce some examples.

    --

    Danny

  • No problem Danny,



    I'm already subscribed to your blogfeed so I won't miss any future examples you publish. Your blog has been a great resource of ideas to me to get the most out of the navigation controls. And if you could write something about designers in the future, that would be great too. :)



    Have fun, Stefaan

  • Dude, that was awesome. Saved me lots of time.

  • Can we add asp webcontrol at the end of each row. In my case, depending upon the business rules i want to add a imagebutton at the end of tree row node and when user click on that image, a dynamic pop up menu will comes up.

    I will appreciate for help

  • This function is definitely called but the templates are alwasy null so the templates I create are ignored.

    protected override void RenderPreText(HtmlTextWriter writer)
    {
    if (PreTextTemplate != null)
    {
    TemplatedTreeNodeTemplateContainer container =
    new TemplatedTreeNodeTemplateContainer(this);
    PreTextTemplate.InstantiateIn(container);
    container.DataBind();
    container.RenderControl(writer);
    }
    base.RenderPreText(writer);
    }

    Any advice on where I went wrong?

  • It's really amazing. Could you please tell me how to include treeview id in TreeNode Rendering?

    Actulay I want my treenode should be render like this


    Prince


    Here, I am able to create the and NodeValue attribute but not able to include TreeViewID because i am not able to get the Treeview object in RenderPreText or RenderPostText method.

  • I want to create an Ajax Treeview custom control. So, how to go about it. Any help would be greately appriciated.

  • Thanks for good sample code. In the past we did someting like

    TreeNode trModuleNode = new TreeNode();

    trModuleNode.Text = "" + objSysModule.moduleName + "" + subChildCount + "";

    This in general works, except the HTML generated might not be well-formed and may result in tool-tip (part of image ALT). That was nasty little bug.

Comments have been disabled for this content.