Patterns for navigation controls, part 1

Most websites need some sort of navigation UI. If you have a site with more than a handful of pages then you’re likely to need to create some components for rendering sets of links that show the structure of your site. So continuing my theme of simple patterns for Sitecore code, here is the first of a few posts on some approaches to navigation.

Basic top-level navigation

The primary navigation on many sites is a fixed set of links showing the top level pages of the site. A very simple control for displaying this can start from the following markup in a user control:

<asp:Repeater runat="server" ID="navRepeater">
    <HeaderTemplate><ul></HeaderTemplate>
    <ItemTemplate>
        <li><asp:HyperLink runat="server" ID="navLink" /></li>
    </ItemTemplate>
    <FooterTemplate></ul></FooterTemplate>
</asp:Repeater>

With some simple code to fill in the data:

protected void Page_Load(object sender, EventArgs e)
{
    var rootItem = Sitecore.Context.Database.GetItem(Sitecore.Context.Site.ContentStartPath);
    navRepeater.DataSource = rootItem.Children;
    navRepeater.ItemDataBound += navRepeater_ItemDataBound;
    navRepeater.DataBind();
}

void navRepeater_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
    if(e.Item.ItemType == ListItemType.AlternatingItem || e.Item.ItemType == ListItemType.Item)
    {
        var item = e.Item.DataItem as Item;
        var navLink = e.Item.FindControl("navLink") as HyperLink;

        navLink.Text = item.DisplayName;
        navLink.NavigateUrl = Sitecore.Links.LinkManager.GetItemUrl(item);
    }
}

The root item of the current site is specified in the <sites> config of your site, which can be accessed via the Sitecore.Context.Site.ContentStartPath property. (Generally this is the right starting place for primary navigation – but if your site’s IA doesn’t follow that pattern you can employ alternative rules, or just use the component’s Data Source here if you want to give editors the power to change this) Loading that item and binding its children to a repeater lets us display a list of links which can be formatted with some CSS.

Adding a second level

Commonly, however, you want your primary nav to be able to show a bit more of the site’s structure. Commonly that means being able to show the immediate children of each of your top level items. This can be achieved by nesting the same behaviour above, to find the children of each of the top level items as it goes through them. The mark-up can be adjusted to add the second level:

<asp:Repeater runat="server" ID="navRepeater">
    <HeaderTemplate><ul></HeaderTemplate>
    <ItemTemplate>
        <li>
            <asp:HyperLink runat="server" ID="navLink" />
            <asp:Repeater runat="server" ID="childRepeater">
                <HeaderTemplate><ul></HeaderTemplate>
                <ItemTemplate>
                    <li><asp:HyperLink runat="server" ID="childLink" /></li>
                </ItemTemplate>
                <FooterTemplate></ul></FooterTemplate>
                </asp:Repeater>
            </asp:Repeater>
        </li>
    </ItemTemplate>
    <FooterTemplate></ul></FooterTemplate>
</asp:Repeater>

And the code similarly:

protected void Page_Load(object sender, EventArgs e)
{
    var rootItem = Sitecore.Context.Database.GetItem(Sitecore.Context.Site.ContentStartPath);
    navRepeater.DataSource = rootItem.Children;
    navRepeater.ItemDataBound += navRepeater_ItemDataBound;
    navRepeater.DataBind();
}

void navRepeater_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
    if(e.Item.ItemType == ListItemType.AlternatingItem || e.Item.ItemType == ListItemType.Item)
    {
        var item = e.Item.DataItem as Item;

        var navLink = e.Item.FindControl("navLink") as HyperLink;
        var childRepeater = e.Item.FindControl("childRepeater") as Repeater;

        navLink.Text = item.DisplayName;
        navLink.NavigateUrl = Sitecore.Links.LinkManager.GetItemUrl(item);

        childRepeater.DataSource = item.Children;
        childRepeater.ItemDataBound += childRepeater_ItemDataBound;
        childRepeater.DataBind();
    }
}

void childRepeater_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
    if (e.Item.ItemType == ListItemType.AlternatingItem || e.Item.ItemType == ListItemType.Item)
    {
        var item = e.Item.DataItem as Item;
        var childLink = e.Item.FindControl("childLink") as HyperLink;

        childLink.Text = item.DisplayName;
        childLink.NavigateUrl = Sitecore.Links.LinkManager.GetItemUrl(item);
    }
}

It’s fairly common to add a bit of CSS and/or jQuery behaviour to make the second level of this data appear and disappear as a hover or click effect. And that behaviour can be applied on top of this mark-up using whatever patterns and styles are appropriate to your site.

Filtering out pages that shouldn’t be shown

One issue this code doesn’t address is that there may be some pages that you need on your site which should not appear in the navigation. Things like login forms, or error pages need to exist in your content tree, but should probably not be shown in the navigation listings. This can be solved fairly simply with a field added to your pages to flag which pages should be shown in navigation. You can create a template to define it like so:

Navigation Metadata

You can then add this template to the inheritance tree for your own page templates.

The code can then be adapted to filter out any items which don’t have the “show in nav” field checked with a simple query instead of processing the Children property of the items:

private string ShowInNavigationQuery = "*[@ShowInNavigation='1']";

protected void Page_Load(object sender, EventArgs e)
{
    var rootItem = Sitecore.Context.Database.GetItem(Sitecore.Context.Site.ContentStartPath);
    navRepeater.DataSource = rootItem.Axes.SelectItems(ShowInNavigationQuery);
    navRepeater.ItemDataBound += navRepeater_ItemDataBound;
    navRepeater.DataBind();
}

void navRepeater_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
    if(e.Item.ItemType == ListItemType.AlternatingItem || e.Item.ItemType == ListItemType.Item)
    {
        var item = e.Item.DataItem as Item;

        var navLink = e.Item.FindControl("navLink") as HyperLink;
        var childRepeater = e.Item.FindControl("childRepeater") as Repeater;

        navLink.Text = item.DisplayName;
        navLink.NavigateUrl = Sitecore.Links.LinkManager.GetItemUrl(item);

        childRepeater.DataSource = item.Axes.SelectItems(ShowInNavigationQuery);
        childRepeater.ItemDataBound += childRepeater_ItemDataBound;
        childRepeater.DataBind();
    }
}

The ShowInNavigationQuery finds all the children of an item which have the “show” flag set to true. This is applied to the two repeater bindings in place of the previous reference to the child items collection.

More on navigation next week.

Advertisements

3 thoughts on “Patterns for navigation controls, part 1

  1. Pingback: Patterns for navigation controls, part 3 | Jeremy Davis
  2. Pingback: Patterns for navigation controls, part 4 | Jeremy Davis
  3. Pingback: Patterns for navigation controls, part 2 | Jeremy Davis

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s