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:

 

posted @ 2009-01-22 09:21  海洋——海纳百川,有容乃大.  阅读(1234)  评论(0编辑  收藏  举报