help.axcms.netAxinom Logo
Save Save Chapter Send Feedback

Developing Navigation in AxCMS.net

How to create your first navigation control

In this article we will develop a simple navigation control and explain the essential navigation concepts.

Pre-Requisites

Dynamic navigation is one of the important advantages of using a CMS.

The hierarchical navigation structure and the assigments of navigation nodes to the content pages in AxCMS.net are managed by the Editors in the Management System, independent of their presentation. Presenting this navigation to the end user depends very much on the project requirements. That is why development of the navigation controls is considered as a project task.

Creating a Simple Navigation Control

Let's start with a very simple navigation control and gradually add more features to it.

First we create a new ASCX-control MyNavigation.ascx and store it under "Controls" (in Template Project). We add this control to the template MyTemplate.aspx created in the previous chapters:

<%@ Register TagPrefix="Sample" TagName="Navigation" Src="~/templates/Controls/MyNavigation.ascx" %>

...
<Sample:Navigation runat="server" id="navigationControl" />

To list the navigation items let's use a simple repeater. It's data source will be a collection of navigation nodes, but for the begining we can mock it.

In MyNavigation.ascx:

<asp:Repeater runat="server" ID="navigationRepeater">
    <ItemTemplate>
        <asp:HyperLink runat="server" ID="navigationHyperLink" /><br />
    </ItemTemplate>
</asp:Repeater>

In MyNavigation.ascx.cs:

    public partial class MyNavigation : System.Web.UI.UserControl
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            List<AxCategory> list = new List<AxCategory>();
            list.Add(new AxCategory("aaa"));
            list.Add(new AxCategory("bbb"));
            list.Add(new AxCategory("ccc"));
            navigationRepeater.DataSource = list;
            navigationRepeater.DataBind();
        }
       
        protected void navigationRepeater_ItemDataBound(object sender, RepeaterItemEventArgs e)
        {
            HyperLink navigationHyperLink = (HyperLink) e.Item.FindControl("navigationHyperLink");
            AxCategory node = (AxCategory)e.Item.DataItem;
            if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
            {
                navigationHyperLink.Text = node.Name;
                navigationHyperLink.NavigateUrl = "http://";
            }
        }

        #region Web Form Designer generated code
        override protected void OnInit(EventArgs e)
        {
            InitializeComponent();
            base.OnInit(e);
        }
        private void InitializeComponent()
        {
            navigationRepeater.ItemDataBound+=new RepeaterItemEventHandler(navigationRepeater_ItemDataBound);
        }
        #endregion
    }

This will produce a list with "aaa", "bbb" and "ccc" linked to nowhere.


Loading Navigation and Assigned Pages

Now let's replace the mock list with the real code which loads navigation items from the database.

We could load navigation items (categories) directly from the database using CategoryAdapter. But we also can take advantage of the fact, that the whole category tree is cached in memory anyway. To access it we use CategoryTreeManager and the Registry-Pattern,

using Axinom.AECMS;
using Axinom.AECMS.Category;
using Axinom.AECMS.Caching;

...

           CategoryTree categoryTree = Registry.GetCategoryTreeManager().Tree;
            AxCategory rootNavigationNode = categoryTree.FindNode("Navigation/www.axinom.com/TOP");
            if (rootNavigationNode != null)
            {
                navigationRepeater.DataSource = rootNavigationNode.Children;
                navigationRepeater.DataBind();
            }

CategoryTree.FindNode allows you to find a node in the tree providing a path of the category names (and/or externIDs). Replace the path with the exact location of your navigation nodes. If you aquire a node from the cached tree, you can rely on the fact, that the property Children is properly initialized. Then we just pass this children collection to the repeater.

Now it renders the real navigation nodes. They are still not linked to the pages and we show only one leven of navigation.

Let's link the nodes to the correct pages. We can do it in a different way. The simplest to understand is - in the Repeater ItemDataBound event we will find the standard page assigned to the current navigation node, load it and take its URL.

using Axinom.AECMS.page;
using Axinom.AECMS.WebControls;

                ElementCollection assignedPages = Classifier.Current.Search((short)ElementTypes.Page, node.AxID, DirectionMode.Forward, false);
                AxPageBaseAdapter pageAdapter = new AxPageBaseAdapter(PageType.Page);
                if (assignedPages.Count > 0)
                {
                    long pageID = assignedPages[0].ID;
                    AxPage standardPage = (AxPage) pageAdapter.Load(pageID);
                    if (standardPage != null)
                    {
                        navigationHyperLink.NavigateUrl = CMSConfigurationSettings.CMSApplicationVirtualPath + standardPage.Url;
                    }
                }

Classifier is the class dealing with category assignments. It can: find elements in a certain category, find categories for a given element, add and remove categories to an element. Classifier follows Singleton pattern.

Normaly there will not be more than one element in the returned collection. We will just take the first element and ignore the others. Classifier returns only the IDs of the assigned elements. To find out the URL of the page we need the full AxPage-object. We load it with AxPageBaseAdapter. To have a full link we need to prefix the page url with  CMSApplicationVirtualPath from Configuration.

If no assigned page is found you can decide to show the navigation item without a link or to hide the item at all.

At this point our navgation control is already operational, but it can be improved in many ways.

Using NavigationManager

Our code so far loads assigned standard page for every navigation node. But AxCMS.net allows to assign also Documents to navigation nodes, not only pages. To be on the safe side, we should remove element type Page in the Classifier.Search, and then depending on the element type returned load either Page or Document.

But we can go a better way. AxCMS.net caches navigation standard assignments for you. You don't even have to know which kind of object is assigned. The pattern for the NavigationManger is the same as for CategoryTreeManager. The code in ItemDataBound will be much more compact:

        protected void navigationRepeater_ItemDataBound(object sender, RepeaterItemEventArgs e)
        {
            HyperLink navigationHyperLink = (HyperLink) e.Item.FindControl("navigationHyperLink");
            AxCategory node = (AxCategory)e.Item.DataItem;
            if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
            {
                navigationHyperLink.Text = node.Name;
                if(Registry.GetNavigationManager().IsRelated(node.AxID))
                {
                    string standardAssignedUrl = Registry.GetNavigationManager().GetURLForNavigationID(node.AxID);
                    navigationHyperLink.NavigateUrl = CMSConfigurationSettings.CMSApplicationVirtualPath + standardAssignedUrl;
                }
            }
        }

NavigationManager.IsRelated indicates, if there is an assigned object at this node at all.

The cache for the NavigationManager and CategoryTreeManager is updated when categories/assignments change (in MS) and when they are published (in LS).

Displaying multiple navigation levels

Currently our control display only the first level of the navigation hierarchy. But there are more. Let's to one step forwards and display all levels at the same time. It means, we need not only the direct children of the root node, but all children. CategoryTree provides the method we need:

        protected void Page_Load(object sender, EventArgs e)
        {
            CategoryTree categoryTree = Registry.GetCategoryTreeManager().Tree;
            AxCategory rootNavigationNode = categoryTree.FindNode("Navigation/www.axinom.com/TOP");
            if (rootNavigationNode != null)
            {
                navigationRepeater.DataSource = categoryTree.GetAllChildren(rootNavigationNode.AxID);
                navigationRepeater.DataBind();
            }
        }

It works, but the result is a mess, because we see all the nodes in one list and cannot recognize any hierarchy.

Luckily AxCategory does ahve a property Level which helps you to show the hierarchy using, say, indentations and/or font size. We respect the separation between the layout and logic. This is why all our code will do is just add a CSS-class to the hyperlink and the formatting is specified then using CSS:

                navigationHyperLink.Text = node.Name;
                navigationHyperLink.CssClass = "myNavigationLevel" + (node.Level - 3 + 1).ToString();

The arithmetic we need to force the first level of the visible navigation to have the number 1 and not the real level coming from the tree.

Now you can attach a CSS-file (reference it from the template or embedd it directly into the template) and specify the formatting rules for the classes myNavigationLevelX. Here is just an example:

a.myNavigationLevel1 { padding-left:  0px; font-size: 16pt;}
a.myNavigationLevel2 { padding-left: 20px; font-size: 14pt;}
a.myNavigationLevel3 { padding-left: 40px; font-size: 12pt;}
a.myNavigationLevel4 { padding-left: 60px; font-size: 10pt;}

With this code in place you see a nice hierarchical navigation, all nodes pointing to their assigned pages/documents.

Highlighting current navigation nodes

You will usually want to show the user, which navigation node is currently active (= user sees a page in this navigation node). If multiple levels of navgation are open, "active node" can change to "active path". How to find out which navigation node the current page belongs to?

Theoretically we could go this way:

Get the current page with AxContext.GetCurrentPage()

  • Lookup the navigation assignment of the current page (Classifier.ClassifiedNodes will help) - there could be multiple assignments, but let's just ignore this special case for now. This assignment we will call active node.
  • Build up the navigation path from the active node to the navigation root. This path we will call active path.
  • Now for every navigation node we render, we check, if this node is on the active path. Methods of the CategoryTree help us to handle these tree operations. If a node is on the active path, we render it with additional CSS class (and designer can define a special formatting for the active nodes)

Looks like a lot of work? Good news: AxCMS.net handles it all for you!

Remember, we inherited the template from TemplateBase instead of System.Web.UI.Page directly? It has namely a property called NavigationContext. This NavigationContext is created and set by AxCMS.net on each request before the template code runs. In Page_Load of your template code you can rely on the properties in NavigationContext. The most important things NavigationContext has to offer:

  • int ActiveID - the ID of the active node (the navigation node where current page is assigned to)
  • ArrayList PathToRoot - active path (from the active node to the navigation root)
  • bool IsInActivePath(int navigationID) - checks if navigationID is in the active path

If you are inside a template you access the navigation context with just a property: this.NavigationContext. If you are in a user control (ASCX), it is a bit more complicated. First, you need to use the Page-property, and then cast it to TemplateBase to get access to the NavigationContext: ((TemplateBase) this.Page).NavigationContext. Other then that it is not complicated. Add this to ItemDataBound handler:

                if (((TemplateBase)this.Page).NavigationContext.IsInActivePath(node.AxID))
                {
                    navigationHyperLink.CssClass += " myNavigationActive";
                }

And define the CSS style myNavigationActive (well, you have better design skills as me):

a.myNavigationActive {border-style:dotted solid; font-weight:bold;}

Using ActiveID as an URL-Parameter

In the most cases AxCMS.net can determine the active node based on the current page navigation assignments. But sometimes the assignment is not unique - one page can be assigned to multiple navigation nodes. For this cases you can simplify the task for AxCMS.net and pass ActiveID of the proper node in the URL. If the parameter ActiveID is present in the URL it takes precedence over all other rules and this node will be highlighted as active.

Another reason to pass ActiveID - it works faster because it saves some database lookup operations.

If you decide to go this way, you should pass ActiveID to every link rendered with your navigation control:

       navigationHyperLink.NavigateUrl = CMSConfigurationSettings.CMSApplicationVirtualPath + standardAssignedUrl + "?ActiveID=" + node.AxID.ToString();

Most sample navigation code from Axinom uses this technique, this is why you see ActiveID= so often in the URLs build with AxCMS.net.

...

It was an introduction into development of navigation controls. There is much more to say about this topic, but we will save it for other articles.