Creating a tree like left menu in Sitecore using UserControls and C#

This post is a follow-up on the article on how to create a tree like left menu in Sitecore using XSLT. This time I will not use XSLT to create my left menu, but instead I will use C# and UserControls.

With Sitecore 5 and Sitecore 6, XSLT’s are becomming deprecated, as it is getting easier and easier to create the same using UserControls.

There are several ways of creating a left menu in C#. In this article I will show how you can dump your left menu into an XML string that can be passed to an asp:XmlDataSource. From the XmlDataSource you can hook the menu into the asp:Menu, the asp:TreeView and even the asp:SiteMap controls. However, none of these controls give you full control of the HTML generated (unless you use these control adaptors), so I will show how to use nested repeaters to create the menu hierachy.

First I must create an XmlLeftMenu class that can dump the current left menu structure into an XML string. This code is an pretty accurate dump of how the XSLT did it:

public class XmlLeftMenu
{
  private Item _root;
  private StringBuilder _sb = new StringBuilder();

  public XmlLeftMenu(Item root)
  {
    _root = root;
  }

  public string LeftMenuXml
  {
    get
    {
      _sb.Append("<menu>");
      RenderMenu(_root);
      _sb.Append("</menu>");
      return _sb.ToString();
    }
  }

  private void RenderMenu(Item root)
  {
    ChildList children = root.GetChildren();
    foreach (Item child in children)
    {
      _sb.AppendFormat(@"<item title=""{0}"" url=""{1}"" haschildren=""{2}"" open=""{3}"" selected=""{4}"">",
      child.DisplayName,
      LinkManager.GetItemUrl(child),
      child.HasChildren,
      child.Axes.IsAncestorOf(Sitecore.Context.Item),
      child.ID == Sitecore.Context.Item.ID);
      if (child.HasChildren && child.Axes.IsAncestorOf(Sitecore.Context.Item))
        RenderMenu(child);
      _sb.Append("</item>");
    }
  }
}

(In this example I simply use the item’s DisplayName as my title, but you should modify the code to select the field containing the page title).

The XML contains the page title and page URL. Haschildren is true if the item has children. Open is true if the current item is a child of the menu item being rendered (indicating that the menu is opened and visible). Selected is true if the menu item is the current item. These last 3 attriubutes can be used to style the menu.

The XmlLeftMenu class can be hooked into a XmlDataSource like this: This is the .aspx page code:

<asp:XmlDataSource ID="xmlMenu" EnableCaching="false" runat="server" XPath="menu/item">
</asp:XmlDataSource>

Please note the EnableCaching=false. This is needed as the control caches the XML by default. If caching is enabled, your menu will not change every time you change page. Here is the codebehind:

protected void Page_Load(object sender, EventArgs e)
{
  XmlLeftMenu leftMenu = new XmlLeftMenu(Sitecore.Context.Database.GetItem(Sitecore.Context.Site.StartPath ));
  xmlMenu.Data = leftMenu.LeftMenuXml;
  DataBind();
}

Now your page has access to the left menu. As said before, you can use the asp:Menu to display the menu directly, but if you would like to keep control of your HTML, you can use a series of nested repeaters like this:

<asp:Repeater ID="repLeftmenu" DataSourceID="xmlMenu" runat="server" EnableViewState="false">
  <HeaderTemplate>
    <ul class="leftMenu">
  </HeaderTemplate>
  <ItemTemplate>
    <li>
      <%-- Rendering level 1 --%>
      <a href="<%# XPath("@url") %>">
        <%# XPath("@title") %>
      </a>
      <%-- Nested repeater rendering level 2 --%>
      <asp:Repeater runat="server" ID="level2" EnableViewState="false" DataSource='<%# XPathSelect("item") %>' Visible='<%# Convert.ToBoolean( XPath("@open") ) %>'>
        <HeaderTemplate>
          <ul>
        </HeaderTemplate>
        <ItemTemplate>
          <li>
            <a href="<%# XPath("@url") %>">
              <%# XPath("@title") %>
            </a>
            <%-- Nested repeater rendering level 3 --%>
            <asp:Repeater runat="server" ID="level3" EnableViewState="false" DataSource='<%# XPathSelect("item") %>' Visible='<%# Convert.ToBoolean( XPath("@open") ) %>'>
              <HeaderTemplate>
                <ul>
              </HeaderTemplate>
              <ItemTemplate>
                <li>
                  <a href="<%# XPath("@url") %>">
                    <%# XPath("@title") %>
                  </a>
                  <%-- Nested repeater rendering level 4
                       You can continue for as many leves as you like...
                  --%>
                  <asp:Repeater runat="server" ID="level4" EnableViewState="false" DataSource='<%# XPathSelect("item") %>' Visible='<%# Convert.ToBoolean( XPath("@open") ) %>'>
                    <HeaderTemplate>
                      <ul>
                    </HeaderTemplate>
                    <ItemTemplate>
                      <li>
                        <a href="<%# XPath("@url") %>">
                          <%# XPath("@title") %>
                        </a>
                      </li>
                    </ItemTemplate>
                    <FooterTemplate>
                      </ul>
                    </FooterTemplate>
                  </asp:Repeater>
                </li>
              </ItemTemplate>
              <FooterTemplate>
                </ul>
              </FooterTemplate>
            </asp:Repeater>
          </li>
        </ItemTemplate>
        <FooterTemplate>
          </ul>
        </FooterTemplate>
      </asp:Repeater>
    </li>
  </ItemTemplate>
  <FooterTemplate>
    </ul>
  </FooterTemplate>
</asp:Repeater>

You can extend the presentation by using the “haschildren”, “open” and “selected” attributes from the XML to create style sheet attributes. Extend each LI tag with the following:

<li class="children<%# XPath(@haschildren") %> open<%# XPath("@open") %> selected<%# XPath("@selected") %>">

This will give you class tags like this childrenTrue and childrenFalse, openTrue and openFalse, selectedTrue and selectedFalse.

Create a Google Style paging component in C#

Several years ago I wrote a an article about Creating a Google Style paging component in XSLT. Some users pointed out to me that the paging component wasn’t exactly Google style. Especially using the arrows didn’t comply with how Google does it, but apart from that it mimicked the Google paging very well.

This is another implementation, this time in C#. Again, I’m not using the Google style exactly. Instead I’m using a “rolling numbers” principle. The component specifies how many numbers should (at most) be present at a time, as demonstated here:

3 types of paging

3 states in the paging component

(Google uses a simple method where it defines how many number should be present before and after the current page (like my first XSLT), but this paging is better at sticking to the middle of a web page).

In this example (and in the code) I specify that I want 10 pages to be present.

Here is the code. First I define a Page class containing the information about a page. This is very simple, but you can build it as advanced as you need it.

/// <summary>
/// Page class containing the information used to create a paging control
/// </summary>
public class Page
{
  /// <summary>
  /// Gets or sets the title.
  /// </summary>
  /// <value>The title.</value>
  public string Title { get; set; }
   
  /// <summary>
  /// Gets or sets the page number.
  /// </summary>
  /// <value>The page num.</value>
  public string PageNum { get; set; }

  /// <summary>
  /// Gets or sets a value indicating whether this page is the current page.
  /// </summary>
  /// <value><c>true</c> if [current page]; otherwise, <c>false</c>.</value>
  public bool CurrentPage { get; set; }
}

Then I have a simple class returning a IEnumerable list of Page classes:

using System.Collections.Generic;

namespace MyProject
{
  /// <summary>
  /// Class generating a enumerable list of pages
  /// </summary>
  /// <remarks>
  /// Primarily used by the thumbnail list to create a paging control
  /// </remarks>
  public class PagingProvider
  {
    /// <summary>
    /// Creates a list of page numbers to be enumerated in a paging control.
    /// </summary>
    /// <remarks>
    /// Paging is 1-based, meaning that the first page is called page 1.
    /// </remarks>
    /// <param name="pageSize">Size of the page.</param>
    /// <param name="totalItems">The total items.</param>
    /// <param name="currentPage">The current page.</param>
    /// <returns></returns>
    public IEnumerable<Page> CreatePages(int pageSize, int totalItems, int currentPage)
    {
      List<Page> pages = new List<Page>();
      int totalPages = (totalItems / pageSize) + 1;
      int startIndex = 0;
      int endIndex = totalPages;
     
      if (totalPages > 10)
      {
        startIndex = currentPage - 5;
        endIndex = currentPage + 5;
        if (startIndex < 0)
        {
          startIndex = 0;
          endIndex = startIndex + 10;
        }
        if (endIndex > totalPages)
        {
          endIndex = totalPages;
          startIndex = totalPages - 10;
        }
      }
      pages.Add(new Page { Title = "««", PageNum = "1", CurrentPage = false });
      if (currentPage == 1)
        pages.Add(new Page { Title = "«", PageNum = (currentPage).ToString(), CurrentPage = false });
      else
        pages.Add(new Page { Title = "«", PageNum = (currentPage - 1).ToString(), CurrentPage = false });
      for (int i=startIndex;i<endIndex;i++)
      {
        Page page = new Page {Title = (i + 1).ToString(), PageNum = (i + 1).ToString(), CurrentPage = i == (currentPage-1)};
        pages.Add(page);
      }
      if (currentPage == totalPages)
        pages.Add(new Page { Title = "»", PageNum = (currentPage).ToString(), CurrentPage = false });
      else
        pages.Add(new Page { Title = "»", PageNum = (currentPage + 1).ToString(), CurrentPage = false });
      pages.Add(new Page { Title = "»»", PageNum = totalPages.ToString(), CurrentPage = false });
      return pages;
    }
  }
}

The function, CreatePages, is called using the information about the data to page, the page size (how many items you have per page), total number of items and the current page. The paging can be hooked to an asp:Repeater for example:

<asp:Repeater ID="repPagesBottom" runat="server" onitemcommand="repPages_ItemCommand">
  <HeaderTemplate>
    <div>
  </HeaderTemplate>
  <ItemTemplate>
    <span>
      <asp:Button CommandName="changePage" CommandArgument='<%# Eval("PageNum") %>' Text='<%# Eval("Title") %>' runat="server" CssClass='<%# Eval("CurrentPage") %>' />
    </span>
   </ItemTemplate>
   <FooterTemplate>
     </div>
   </FooterTemplate>
</asp:Repeater>

Simply apply the output from the CreatePages to the Datasource of the repeater.

Automatically create TabIndex in a repeater

In order to conform with the Section 508 Accessibility guidelines, and the WCAG Guidelines, you must provide a tabindex for all of your buttons, checkboxes etc.
So what should you do if you create these elements within a Repeater List Control? Then you don’t know how many buttons, checkboxes etc. you have.

The solution is to create a code-behind property (or function) and then call it using the <%# … %> syntax. Please observe the following example:

<asp:Repeater ID="repGroups" runat="server">
  <HeaderTemplate>
   
<ul>
  </HeaderTemplate>
  <ItemTemplate>
   
	<li>      
     
<div>
        <asp:CheckBox ID="cbGroup" Checked="true" runat="server" 
         TabIndex='<%# TabIndex %>' Text='<%# Eval("Title") %>' />
     </div>
   </li>
  </ItemTemplate>
  <FooterTemplate>
   </ul>
  </FooterTemplate>
</asp:Repeater>

Notice that asp:CheckBox uses the TabIndex=’<%# TabIndex %>’ to call a property on the page’s codebehind file. This property simply increases an integer and returns the new value:

private int _tabIndex = 0;

public int TabIndex
{
  get 
  { 
    _tabIndex++;
    return _tabIndex;
  }
}

The private variable _tabIndex is reset each time the page is called, and the TabIndex property returns a new value each time it is called, giving you tabindexes from 1 to infinite.

Follow

Get every new post delivered to your Inbox.

Join 92 other followers