Creating custom multi-level trees in Umbraco

How to make a multi-level database driven custom tree in Umbraco
May 27 2011
cms    umbraco

This is the third post in a series.  If you haven't already, read the previous post on creating a custom content tree and section in umbraco

In my last post I created a custom section and basic tree for my atomicf1.com website.  In this blog I’ll create a more complex nested tree in the “Results” section.  This tree is allows site editors/administrators to maintain race results and register drivers for competition in a season.  Only drivers registered for the particular season will be able to have results entered against races.  The races for a given season are defined in the Data section created in the previous post and the Results section will use that information to build the basics of the tree.  I realise all this could be done the main content tree, but the exercise is more about me learning about custom sections and trees in Umbraco using a subject (Formula 1) that motivates me.

What we want is a content tree that looks like the following, where we have all Seasons defined in the Data section and a place to register entrants and enter race results for each of the races defined for that season.

Races

How to build a simple tree is covered in detailed in the previous post which I’ll assume has been read prior to this.  To recap, we need to implement to do the follow:

  • Define the new section in the umbracoApp table
  • Create a single entry for the root “Results” in the umbracoAppTree table.
  • Add a new section to the desired language files
  • Derive a class from umbraco.cms.presentation.Trees.BaseTree to populate the tree
  • Implement classes from the umbraco.interfaces.ITaskReturnUrl interface to create nodes for the tree where custom controls are not used
  • Create a Page to maintain each item
  • Create custom User Controls for new items where required
  • Define the nods in the UI.xml file for the custom user controls

 

The first difference to note in this implementation is that we only require one entry in the umbracoAppTree table, that for the root node “Results”.  Because the other nodes in the tree are generated from data entered in the Seasons subtree in the Data section we don’t need to create and remove entries from the umbracoAppTable for each of those.

My implementation of umbraco.cms.presentation.Trees.BaseTree looks like the following

namespace atomicf1.cms.presentation.Trees
{
    public class loadResults : BaseTree
    {
        private ISeasonRepository _seasonRepository;
        
        public loadResults(string application) : base(application)
        {
            _seasonRepository = new SeasonRepository();
        }

        protected override void CreateRootNode(ref XmlTreeNode rootNode)
        {
            rootNode.Icon = FolderIcon;
            rootNode.OpenIcon = FolderIconOpen;
            rootNode.NodeType = TreeAlias;
            rootNode.NodeID = "-1";

            rootNode.Menu.Clear();
            rootNode.Menu.Add(ActionRefresh.Instance);
        }

        public override void Render(ref XmlTree tree)
        {
            TreeService treeService;

            if (this.NodeKey == string.Empty) {
                
                PopulateSeasons(ref tree);

            }
            else
            {
                string keyType = this.NodeKey.Split(new string[] { "-" }, StringSplitOptions.RemoveEmptyEntries)[0];
                int keyId = int.Parse(this.NodeKey.Split(new string[] { "-" }, StringSplitOptions.RemoveEmptyEntries)[1]);
                
                var factory = new LoadResultsTreeCommandFactory(this);
                var command = factory.GetLoader(keyType);
                command.Populate(ref tree, keyId);
            }
            
        }

        protected void PopulateSeasons(ref XmlTree tree)
        {
            var seasons = _seasonRepository.GetAll();

            foreach (var season in seasons)
            {

                var node = XmlTreeNode.Create(this);
                node.NodeID = season.Id.ToString();
                node.Text = season.Name;
                node.Icon = "folder.gif";
                node.OpenIcon = "folder_o.gif";
                //node.Action = "javascript:alert('boo')";                    

                var treeService = new TreeService(-1, TreeAlias, ShowContextMenu, IsDialog, DialogMode, app, string.Format("Season-{0}", season.Id));
                node.Source = treeService.GetServiceUrl();

                node.Menu.Clear();
                node.Menu.Add(ActionRefresh.Instance);
                
                tree.Add(node);

            }
        }
              
        public override void RenderJS(ref System.Text.StringBuilder Javascript)
        {
            Javascript.Append(
               @"
                    function openSeasonEntry(id) 
                    {
                        parent.right.document.location.href = 'plugins/atomicf1/editSeasonEntry.aspx?id=' + id;
                    }
                    function openRaceEntry(id, raceid)
                    {
                        parent.right.document.location.href = 'plugins/atomicf1/editRaceEntry.aspx?id=' + id + '&raceid=' + raceid;
                    }                    
                ");
        }
    }
}

 

If you think it looks a little sparse to be creating all the nodes in a tree that’s 6 levels deep, you’d be right, but more on that later.  The main “trick” to point out is that you need to call TreeService.GetServiceUrl to have your tree know how to create it’s child node.  Within the TreeService constructor most arguments can be left as they are above, with the last being the one that you need to set for each for each parent node.  This value is how your code will determine what sub-tree needs to be created in the Render method and what Id to use to populate it.  In my case I’m saying create a Season substree for the season with Id season.Id.  You will also need to extract the sub-tree type and id inside the Render method

string keyType = this.NodeKey.Split(new string[] { "-" }, StringSplitOptions.RemoveEmptyEntries)[0];
int keyId = int.Parse(this.NodeKey.Split(new string[] { "-" }, StringSplitOptions.RemoveEmptyEntries)[1]);

Using the above two pieces of key information (pardon the pun) I can determine how my sub-tree is going to be built.  I’ve used a builder/strategy pattern rather than include all the code I need to generate al levels of my tree inside the one Render method and class.

 

namespace atomicf1.cms.presentation.Trees
{
    public class LoadResultsTreeCommandFactory
    {
        private BaseTree _baseTree;

        public LoadResultsTreeCommandFactory(BaseTree baseTree)
        {
            _baseTree = baseTree;
        }

        public ILoadResultsTreeCommand GetLoader(string subtreeKey)
        {
            switch (subtreeKey) {

                case "Season":
                    return new loadResultsSeason(_baseTree);                    
                case "Entries":
                    return new loadResultsEntries(_baseTree);
                case "Races":
                    return new loadResultsRaces(_baseTree);
                case "RaceEntry":
                    return new loadResultsRaceEntries(_baseTree);                
            }

            return null;
        }
    }
}

The above class determines which tree Loader to use.  Nothing particularly Umbraco here…

I’ve created a BaseLoadResultsTreeCommand to handle all the common stuff.

namespace atomicf1.cms.presentation.Trees
{
    public abstract class BaseLoadResultsTreeCommand : ILoadResultsTreeCommand
    {
        protected BaseTree _baseTree;
        protected ISeasonRepository _seasonRepository;

        public BaseLoadResultsTreeCommand(BaseTree tree)
        {
            _baseTree = tree;
            _seasonRepository = new SeasonRepository();
        }

        public abstract void Populate(ref XmlTree tree, int keyId);

        public TreeService GetTreeService(int keyId, string nodeKey)
        {
            return new TreeService(keyId, _baseTree.TreeAlias, _baseTree.ShowContextMenu, _baseTree.IsDialog, _baseTree.DialogMode, _baseTree.app, nodeKey);
        }
    }
}
namespace atomicf1.cms.presentation.Trees
{
    public interface ILoadResultsTreeCommand
    {
        void Populate(ref XmlTree tree, int keyId);
    }
}

Here’s how my loader for each Season subtree looks

namespace atomicf1.cms.presentation.Trees
{
    public class loadResultsSeason : BaseLoadResultsTreeCommand, ILoadResultsTreeCommand
    {
        public loadResultsSeason(BaseTree tree) : base(tree) { }
        
        #region ILoadResultsTreeCommand Members

        public override void Populate(ref XmlTree tree, int keyId)
        {
            Season season = _seasonRepository.GetById(keyId);

            if (season != null)
            {

                XmlTreeNode entries = XmlTreeNode.Create(_baseTree);
                entries.NodeID = season.Id.ToString();
                entries.Icon = "folder.gif";
                entries.Text = "Entrants";
                entries.NodeType = "seasonEntry";

                var treeService = GetTreeService(keyId, string.Format("Entries-{0}", season.Id));
                entries.Source = season.Entrants.Count() > 0 ? treeService.GetServiceUrl() : "";

                entries.Menu.Clear();
                entries.Menu.AddRange(new List<IAction> { ActionNew.Instance, ContextMenuSeperator.Instance, ActionRefresh.Instance });

                tree.Add(entries);

                XmlTreeNode races = XmlTreeNode.Create(_baseTree);
                races.NodeID = season.Id.ToString();
                races.Icon = "folder.gif";
                races.Text = "Races";
                races.NodeType = "seasonRaceFolder";

                treeService = GetTreeService(keyId, string.Format("Races-{0}", season.Id));
                races.Source = season.Races.Count() > 0 ? treeService.GetServiceUrl() : "";

                races.Menu.Clear();
                races.Menu.AddRange(new List<IAction> { ActionRefresh.Instance });

                tree.Add(races);

            }
        }

        #endregion
    }
}

I take the supplied database key for the season and retrieve my season from the repository and then create the Entrants and Races nodes.  As mentioned earlier, it’s important to provide the keyType and keyId for the for the next node down the tree.  Remember that the original Render method, for the loadResults class registered in the umbracoAppTree will be the one that’s called each time and it will determine which class to load to render the appropriate node and it’s children.

 

That’s it!  There’s not a lot of difference between what I did in the previous post and this one.  Just the small tweaking to link up the relationships between parent and child trees.  Once complete, my final tree looks like

Entrants

Post a comment

comments powered by Disqus