Why should you care?

With almost all sites, you will have a main navigation, and more often than not you will want the navigation structure to reflect the structure of your pages how they are in the content management system.

It is tempting in Umbraco to write razor code to loop through your pages and generate the navigation on the fly. This is bad practice because it will do this for each page view, and if you have a large site or many visitors, it will be unnecessary repeat work, and could possible make your site run slower.

It is better practice to generate a model and pass that into the view. Once you have the navigation in the model, you can use caching to store it in memory and call it when you need it.

This post gives you the basic code to loop through the pages in your site and populate the navigation model. It also gives you the code to add it to and call it from the cache.

Models

namespace CodeShare.Web.Models
{
    public class NavigationLink
    {
        public string Text { get; set; }
        public string Url { get; set; }
        public bool NewWindow { get; set; }
        public string Target {  get { return NewWindow ? "_blank" : null; } }
        public string Title { get; set; }

        public NavigationLink()
        { }
        public NavigationLink(string url, string text = null, bool newWindow = false, string title = null)
        {
            Text = text;
            Url = url;
            NewWindow = newWindow;
            Title = title;
        }
    }
}

using System.Collections.Generic;
using System.Linq;

namespace CodeShare.Web.Models
{
    public class NavigationListItem
    {
        public string Text { get; set; }
        public NavigationLink Link { get; set; }
        public List<NavigationListItem> Items { get; set; }
        public bool HasChildren { get { return Items != null && Items.Any() && Items.Count > 0;  } }

        public NavigationListItem()
        { }
        public NavigationListItem(NavigationLink link)
        {
            Link = link;
        }

        public NavigationListItem(string text)
        {
            Text = text;
        }
    }
}

Controller

using System;
using System.Collections.Generic;
using System.Web.Mvc;
using Umbraco.Web.Mvc;
using CodeShare.Web.Models;
using Umbraco.Core.Models;
using System.Runtime.Caching;

namespace CodeShare.Web.Controllers
{
    public class SiteLayoutController : SurfaceController
    {
        public const string VIEW_FOLDER_PATH = "~/Views/Partials/SiteLayout/";

        /// <summary>
        /// Renders the top navigation partial
        /// </summary>
        /// <returns>Partial view with a model</returns>
        public ActionResult RenderTopNavigation()
        {
            List<NavigationListItem> nav = GetNavigationModelFromDatabase();
            return PartialView(VIEW_FOLDER_PATH + "_TopNavigation.cshtml", nav);
        }

        /// <summary>
        /// Finds the home page and gets the navigation structure based on it and it's children
        /// </summary>
        /// <returns>A List of NavigationListItems, representing the structure of the site.</returns>
        private List<NavigationListItem> GetNavigationModelFromDatabase()
        {
            const int HOME_PAGE_POSITION_IN_PATH = 2;
            int homePageId = int.Parse(CurrentPage.Path.Split(',')[HOME_PAGE_POSITION_IN_PATH]);
            IPublishedContent homePage = Umbraco.Content(homePageId);
            List<NavigationListItem> nav = new List<NavigationListItem>();
            nav.Add(new NavigationListItem(new NavigationLink(homePage.Url, homePage.Name)));
            nav.AddRange(GetChildNavigationList(homePage));
            return nav;
        }

        /// <summary>
        /// Loops through the child pages of a given page and their children to get the structure of the site.
        /// </summary>
        /// <param name="page">The parent page which you want the child structure for</param>
        /// <returns>A List of NavigationListItems, representing the structure of the pages below a page.</returns>
        private List<NavigationListItem> GetChildNavigationList(dynamic page)
        {
            List<NavigationListItem> listItems = null;
            var childPages = page.Children.Where("Visible");
            if(childPages != null && childPages.Any() && childPages.Count() > 0)
            {
                listItems = new List<NavigationListItem>();
                foreach(var childPage in childPages)
                {
                    NavigationListItem listItem = new NavigationListItem(new NavigationLink(childPage.Url, childPage.Name));
                    listItem.Items = GetChildNavigationList(childPage);
                    listItems.Add(listItem);
                }
            }
            return listItems;
        }
    }
}

Partial View

Here is the partial view to render the navigation model.

@inherits Umbraco.Web.Mvc.UmbracoViewPage<List<NavigationListItem>>
@using CodeShare.Web.Models
<nav id="menu-wrap" role="navigation">
    <ul class="menu" id="primary-menu">
        @RenderChildItems(Model)
    </ul>
</nav>
@helper RenderChildItems(List<NavigationListItem> listItems)
{
    if (listItems != null)
    {
        foreach (var item in listItems)
        {
            <li>
                @if (!String.IsNullOrEmpty(item.Text))
                {
                    @item.Text
                }
                else if (item.Link != null)
                {
                    <a href="@item.Link.Url" class="@(Umbraco.AssignedContentItem.Url == item.Link.Url ? "active" : null) @(item.HasChildren ? "fh5co-sub-ddown" : null)" target="@item.Link.Target">@item.Link.Text</a>
                }

                @if (item.HasChildren)
                {
                    <ul class="sub-menu">
                        @RenderChildItems(item.Items)
                    </ul>
                }
            </li>
        }       
    }
}

View

Here is how you call it from the view.

@{Html.RenderAction("RenderTopNavigation", "SiteLayout");}

Using Caching

Here is how you would use caching for the navigation model:

using System;
using System.Collections.Generic;
using System.Web.Mvc;
using Umbraco.Web.Mvc;
using CodeShare.Web.Models;
using Umbraco.Core.Models;
using System.Runtime.Caching;

namespace CodeShare.Web.Controllers
{
    public class SiteLayoutController : SurfaceController
    {
        public const string VIEW_FOLDER_PATH = "~/Views/Partials/SiteLayout/";

        /// <summary>
        /// Renders the top navigation partial
        /// </summary>
        /// <returns>Partial view with a model</returns>
        public ActionResult RenderTopNavigation()
        {
            List<NavigationListItem> nav = GetObjectFromCache<List<NavigationListItem>>("mainNav", 5, GetNavigationModelFromDatabase);
            return PartialView(VIEW_FOLDER_PATH + "_TopNavigation.cshtml", nav);
        }

        /// <summary>
        /// A generic function for getting and setting objects to the memory cache.
        /// </summary>
        /// <typeparam name="T">The type of the object to be returned.</typeparam>
        /// <param name="cacheItemName">The name to be used when storing this object in the cache.</param>
        /// <param name="cacheTimeInMinutes">How long to cache this object for.</param>
        /// <param name="objectSettingFunction">A parameterless function to call if the object isn't in the cache and you need to set it.</param>
        /// <returns>An object of the type you asked for</returns>
        private static T GetObjectFromCache<T>(string cacheItemName, int cacheTimeInMinutes, Func<T> objectSettingFunction)
        {
            ObjectCache cache = MemoryCache.Default;
            var cachedObject = (T)cache[cacheItemName];
            if(cachedObject == null)
            {
                CacheItemPolicy policy = new CacheItemPolicy();
                policy.AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(cacheTimeInMinutes);
                cachedObject = objectSettingFunction();
                cache.Set(cacheItemName, cachedObject, policy);
            }
            return cachedObject;
        }
    }
}

If you would like to know more about this caching method, read my other blog post about it.

Want to thank me?

If I've helped you out and you want to thank me, why not buy me a coffee?

Paul Seal

A .NET Web Developer from Derby (UK) who specialises in building Content Management System (CMS) websites using MVC with Umbraco as a framework. Paul is passionate about web development and programming as a whole. Apart from when he's with his wife and son, if he's not writing code, he's thinking about it or listening to a podcast about it.

Web: codeshare.co.uk

Comments

Share Top