technology from back to front

A Custom ASP.Net Navigation Component for EpiServer CMS

LShift have used the EpiServer CMS on several customer projects and it generally does most things you would want
to do with a CMS in a simple way. EpiServer is a .Net based CMS and if you understand ASP.NET templated pages and templated controls it is very straightforward with a minimal learning curve.

One challenge I faced on a recent project was to implement a particular HTML navigation design using EpiServer. The HTML design called for the navigation to be rendered as nested HTML lists with the current section of the site annotated with a particular class.

For example if you were looking at “Tasty Fish” in the “Cat Food” section of the site the HTML
should look something like this:

<ul>
 <li>Dog Food
      <ul>
          <li>Meaty Bones</li>
        </ul>
 </li>
 <li class="selected">Cat Food
     <ul>
          <li>Tasty Fish</li>
     </ul>
 </li>
</ul>

On initial inspection the EpiServer CMS appears to have two controls that may help, the EpiServer:MenuList and the EpiServer:PageTree. I first attempted to use the EpiServer:MenuList, this allowed me to do this:

   <ul>
      <li>Dog Food</li>
       <li>Cat Food</li>
   </ul>
 <ul>
      <li class="selected">Tasty Fish</li>
    </ul>

This isn’t quite what the design required, the complete site navigation tree needed to be rendered since CSS was being used to show and hide menus in response to mouse rollovers.

So for attempt two I tried the EpiServer:PageTree component; this component is designed to render a whole tree of pages so it should be an appropriate solution. It is a very flexible component and provides lots of templates for customising the layout based upon the state of the tree. This is what I ended up with:

 <ul>
      <li>Dog Food
          <ul>
              <li>Meaty Bones</li>
            </ul>
     </li>
     <li>Cat Food
          <ul>
              <li class="selected">Tasty Fish</li>  <!-- OH NO THIS IS WRONG -->
            </ul>
     </li>
 </ul>

This was very close! However it didn’t meet the design requirement; the top level item that contained the current page needed to be tagged with the CSS class, not the item corresponding to the current page. There didn’t seem to be an easy way to achieve this with the EPiServer components.

I decided I probably need some type of custom control, I then proceeded to write three implementations of a navigation control moving from sinful generation of HTML in a code behind, through my own templated control until arriving at the obvious solution using the asp:ListView control and a simple code behind. This was a nice solution because it uses a standard ASP.NET component in a standard way, the complication of tagging the selected top level item could be hidden away in a small code behind, and the markup was completely under the control of the HTML developer.

The navigation section of the ASP page looked like this:

   <asp:ListView ID="Level1" runat="server" ItemPlaceHolderID="Level1Item">
      <LayoutTemplate>
          <ul><asp:PlaceHolder ID="Level1Item" runat="server"/></ul>
        </LayoutTemplate>
     <ItemTemplate>
            <li class='<%# ((Boolean)Eval("Selected")) ? "selected" : "" %>'><%# Eval("Name") %>
                <asp:ListView ID="Level2" runat="server" ItemPlaceHolderID="Level2Item">
                  <LayoutTemplate>
                      <ul><asp:PlaceHolder ID="Level2Item" runat="server"/></ul>
                    </LayoutTemplate>
                 <ItemTemplate>
                        <li><%# Eval("Name") %>
                 </ItemTemplate>
               </asp:ListView>
           </li>
     </ItemTemplate>
   </asp:ListView>

This is a straightforward usage of nested ListViews and ASP data binding expressions, all of the markup is visible and it can be explained to an HTML developer in a short amount of time. New navigation levels can be added in exactly the same way that the Level 2 navigation was added to the Level1 navigation. The ternary operator within the data binding expression, class='<%# ((Boolean)Eval("Selected")) ? "selected" : "" %>', determines if the navigation item is selected, this is a standard mechanism for conditional rendering with ASP.NET data bound controls.

This was combined with a page behind like this:

 protected override void OnLoad(System.EventArgs e)
  {
       base.OnLoad(e);

     Level1.DataSource = BuildMenuItems();
       Level1.DataBind();
  }

   private List<MenuItem> BuildMenuItems()
   {
       List<MenuItem> menuItems = new List<MenuItem>();

        PageData homePage = GetPage(PageReference.StartPage);
       foreach(PageData child in GetChildren(homePage.PageLink))
       {
           if(child.VisibleInMenu)
         {
               MenuItem item = CreateMenuItem(child, true);
                item.Selected = findPage(CurrentPage.PageGuid, child);
              menuItems.Add(item);
            }
       }

       return menuItems;
   }

   private MenuItem CreateMenuItem(PageData page, Boolean includeChildren)
 {
       MenuItem item = new MenuItem(page.PageName);
        item.Url = page.LinkURL;

        if (includeChildren)
        {
           PageDataCollection children = GetChildren(page.PageLink);
           foreach (PageData child in children)
            {
               if (child.VisibleInMenu)
                {
                   item.Children.Add(CreateMenuItem(child, true));
             }
           }
       }

       return item;
    }

   private Boolean findPage(Guid id, PageData parent)
  {
       if (id == parent.PageGuid) return true;

     foreach (PageData page in GetChildren(parent.PageLink))
     {
           if (page.PageGuid == id)
            {
               return true;
            }
           if(findPage(id, page))
          {
               return true;
            }
       }

       return false;
   }

With a helper class MenuItem defined like this:

public class MenuItem
{
  public MenuItem(String name)
    {
       this.Name = name;
   }

   public String Name { get; set; }
    public String Url { get; set; }
 public Boolean Selected { get; set; }
   private List<MenuItem> children = new List<MenuItem>();
 public List<MenuItem> Children {
      get
     {
           return children;
        }
       set
     {
           children = value;
       }

   }

}

The page behind creates MenuItem instances for each page in the navigation. The top level item gets tagged as selected only if the current page is one of its children. This is a reasonable amount of code to write but it was the smallest solution that solved the problem and made the HTML obvious and available for modification by HTML developers.

by
tim
on
29/11/09
  1. I like this solution, it’s clean and the markup is great, too. Not very often you see such things.

    Keep it up!

    (And please provide a download link next time :)

 
 


nine + 4 =

2000-14 LShift Ltd, 1st Floor, Hoxton Point, 6 Rufus Street, London, N1 6PE, UK+44 (0)20 7729 7060   Contact us