Archive for September, 2006

MOSS 2007 WCM Development Part 1 – Custom Breadcrumb control

In the course of working with Beta 2 and now Beta 2 TR, we ran into a few development challenges. Some are easily fixable, some requires more thoughts. In this series of blog entries, I will attempt to document the challenges we ran into and any solutions we devised on the way.

Variation Feature in MOSS

The variation feature in MOSS is created to address symmetrical site hierarchy structure for multiple languages or multiple devices. For instance, if a site needs to provide content in both English and French languages, the variation feature in MOSS can be used to provide:

  • Variable label creation, this sets up labels to be used in a site structure to logically separate the content for different languages.
  • Variation hierarchy creation, this takes an existing hierarchy that you specify (for example from the “/” of the site) and move it under a source label structure, then replicate the same structure to the target label(s).
  • Workflow for content publishing that works with variation labels.

I am not going to go into the details of the variation feature, you can find out all about them from the MSDN site or download the SDK.


Challenge of the default Site Map Path Control

The challenge we ran into after setting up variation labels is that the variation labels are always displayed on the default out-of-the-box site map path control (aka breadcrumb). For example, if the source variation label is “English” and the target variation label is “French”, when a user navigates to the root of the site, the user will automatically be redirected to the proper variation label site. This is done via the Variation Root logic which will detect the locale via the browser, and if the locale matches one of the variation labels, the user will be redirected to that label. And if no variation labels match the locale, then the user will be redirected to the source label. All this is fine except the variation is a site level being created under root, the page that the user is redirected to is no longer at the root of the site. That is, if a user is redirected to the “Français” site, the hierarchy being displayed on the site map will be:

french01.jpg

This is not a desirable condition because:

  • The user wanted to go to the home page of the site, but instead the user is redirected to a sub site under root.
  • The extra variation site structure does not add value to the browsing experience
  • The default.aspx page at the root level can not be navigated to because of the variation root redirect behaviour, and displaying the Home link on the site map which redirects the users back to the variation label level (e.g. Français) can be confusing.

The desired functionality of the site map is simply to skip the rendering of the variation labels so that the net effect of a user navigating to the root will just be silently redirected to the variation label site, but not giving the user a visual clue the actual hierarchy level they are at. That is, if a user is to be redirected to the Français variation site, the site map path should still only render:

french02.jpg

as the current site level to the user because that is the context the user is expecting.


The Solution

To do this I created a custom site map path web control that:

  1. Can trim the variation labels (or any node name for that matters) using the “IgnoreNodeName” attribute.
  2. Can specify the separator string using “Separator” attribute. For example: ” >> “.
  3. Can specify the root node image url using “RootNodeImageUrl” attribute so that we don’t have to render the root collection name.
  4. Can specify not to render the current location as a link using “RenderCurrentNodeAsLink” attribute. When set to false, the current node will be rendered as a label.
  5. Can specify a CSS Style for the current node item using “CurrentNodeStyleCssClass” attribute.

Some of the features are already in the ASP.NET Site Map Path control. The key new features are the first 3 features. The result is a site map path control that allows me to specify which node to skip rendering, as well as giving enough control over how the site map path is rendered.


Development Method

Since MOSS is built on top of ASP.NET 2.0, a web custom control written for an ASP.NET 2.0 web site will also work inside MOSS. To ease development, I first set up an ASP.NET web site that mimics the MOSS 2007 site structure and file structure. Then, I created the web custom control and tested and debugged it inside the ASP.NET web site before deploying it to MOSS. The pieces include:

  • Create a new web site using VS2005.
  • Set up a master page to be used with the content pages, MOSS 2007 makes extensive use of master pages.
  • Set up some sample ASPX pages that use the master page file.
  • Set up a web.sitemap file as the Site Map data source, referencing the sample ASPX pages just created.
  • Create the custom site map path control in a separate project.
  • Register the custom site map path DLL with the master page, and place the control on the master page.
  • Assign values to the different attributes to test the behaviours. Debug then test again.

Once the custom control is working in an ordinary ASP.NET web site, it is time to test it in the MOSS environment. Here are the steps I use:

  1. Copy the DLL of the custom control to the GAC of the MOSS server.
  2. Edit the web.config file of the SharePoint application and add the custom control to the SafeControlList.
  3. Use SharePoint Designer, open a Master Page and save it using a different name to avoid messing up the out-of-the-box master page.
  4. Add a Page directive to register the assembly to the master page and define a tag name.
  5. Add the control to the page by using the defined tag name:control name syntax.
  6. Save the master page and check it in as a major version, and approve the changes to make it public.
  7. Navigate to a site in your MOSS application. Click on Site Action and select Settings | Modify all settings.
    modifyallsitesettings.jpg
  8. Click Master page link under the Look and Feel Group
    masterpage.jpg
  9. Select the newly saved master page, and select the Reset all subsites to inherit this Site Master Page setting checkbox.
    selectmasterpage1.jpg
  10. Click OK to apply the master page.

If all steps are performed successfully, you will see your site having the newly added custom control on the page.

The Result

To illustrate the point, I edited the master page so that the new custom site map path and the original site map path are displayed one on top of the other. As you can see, the top custom site map path is a lot more concise and does not display the unnecessary variation label.

compare.jpg

Updates:
You can view the source code of the custom site map path control:


using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace KamLau.WebControls
{
  [DefaultProperty("RenderCurrentNode")]
  [ToolboxData("")]
  public class CustomBreadcrumb : WebControl
  {
    # region Properties

    ///
    /// This attribute is created to accomodate using an image (like a house for example) to represent the root node.
    /// If no value is specified, the textual representation of the root node is used.  Otherwise the image is displayed.
    ///
    private const string defaultRootNodeImageUrl = "";
    private string rootNodeImageUrl = defaultRootNodeImageUrl;
    [Category("Appearance")]
    [DefaultValue(defaultRootNodeImageUrl)]
    [Description("A url of an image to be displayed instead of a text label/hyperlink for the root node.")]
    [Browsable(true)]
    public string RootNodeImageUrl
    {
      get { return rootNodeImageUrl; }
      set { rootNodeImageUrl = value; }
    }

    ///
    /// The string that will be used as the separator of the breadcrumb nodes.  If no value is specified, ">" will be used.
    ///
    private const string defaultSeparator = ">";
    private string separator = defaultSeparator;
    [Category("Appearance")]
    [DefaultValue(defaultSeparator)]
    [Description("A string used as separators between breadcrumb nodes.")]
    [Browsable(true)]
    public string Separator
    {
      get { return separator; }
      set { separator = value; }
    }

    ///
    /// A boolean to indicate if the current node should be rendered as a hyperlink.  Default value for this property is false.
    ///
    private const bool defaultRenderCurrentNodeAsLink = false;
    private bool renderCurrentNodeAsLink = defaultRenderCurrentNodeAsLink;
    [Category("Appearance")]
    [DefaultValue(defaultRenderCurrentNodeAsLink)]
    [Description("Indicates if the current node should be rendered as hyperlink.")]
    [Browsable(true)]
    public bool RenderCurrentNodeAsLink
    {
      get { return renderCurrentNodeAsLink; }
      set { renderCurrentNodeAsLink = value; }
    }

    private const string defaultCurrentNodeStyleCssClass = "";
    private string currentNodeStyleCssClass = defaultCurrentNodeStyleCssClass;
    [Category("Appearance")]
    [DefaultValue(defaultCurrentNodeStyleCssClass)]
    [Description("The CSS style to use with the current node.")]
    [Browsable(true)]
    public string CurrentNodeStyleCssClass
    {
      get { return currentNodeStyleCssClass; }
      set { currentNodeStyleCssClass = value; }
    }

    ///
    /// A string with comma delimited values which will be ignored when the breadcrumb is rendered.  This is especially useful
    /// for trimming the variation labels in MOSS2007, or if any level of the hierarchy in the navigation needs to be skipped.
    ///
    private const string defaultIgnoreNodeName = "";
    private string ignoreNodeName = defaultIgnoreNodeName;
    [Category("Appearance")]
    [DefaultValue(defaultIgnoreNodeName)]
    [Description("The substrings (name of navigation nodes) in this comma delimited string will not be rendered on the breadcrumb.")]
    public string IgnoreNodeName
    {
      get { return ignoreNodeName; }
      set { ignoreNodeName = value; }
    }

    private const string defaultSiteMapProvider = "AspnetXmlSiteMapProvider";
    private string siteMapProvider = defaultSiteMapProvider;
    [Category("Appearance")]
    [DefaultValue(defaultSiteMapProvider)]
    [Description("Specify the name of the current site map provider")]
    public string SiteMapProvider
    {
      get { return siteMapProvider; }
      set { siteMapProvider = value; }
    }

    #endregion 

    Stack LinkStack = new Stack();

    ///
    /// Create HyperLink objects and pop to the stack, recursively going up the hierarchy
    ///
    ///
    ///
    private void TraverseUp(SiteMapNode currentNode, Stack linkStack)
    {
      if (currentNode != null)
      {
        HyperLink currentLink = new HyperLink();
        currentLink.Text = currentNode.Title;
        currentLink.NavigateUrl = currentNode.Url;
        currentLink.Attributes.Add("alt", currentNode.Description);
        linkStack.Push(currentLink);
        if (currentNode.ParentNode != null)
        {
          TraverseUp(currentNode.ParentNode, linkStack);
        }
      }
    }

    protected override void RenderContents(HtmlTextWriter output)
    {
      TraverseUp(SiteMap.CurrentNode, LinkStack);
      bool isRoot = true;
      while(LinkStack.Count > 0)
      {
        Label spanTag = new Label(); // use label to wrap  tags around nodes.
        HyperLink nodeLink = (HyperLink) LinkStack.Pop(); // use a stack (FILO) to reverse the order of traversing up the hierarchy

        if (!IsIgnore(nodeLink.Text))
        {
          if (!isRoot)
          {
            Label separatorLabel = new Label();
            separatorLabel.Text = Separator;
            separatorLabel.RenderControl(output);
          }
          if (LinkStack.Count == 0 && !RenderCurrentNodeAsLink)
          {
            if (RootNodeImageUrl != String.Empty && isRoot)
            {
              Image rootNodeImage = new Image();
              rootNodeImage.ImageUrl = RootNodeImageUrl;
              rootNodeImage.RenderControl(output);
            }
            else
            {
              spanTag.Text = nodeLink.Text;
              spanTag.CssClass = CurrentNodeStyleCssClass;
              spanTag.RenderControl(output);
            }
          }
          else
          {
            if (RootNodeImageUrl != String.Empty && isRoot)
            {
              nodeLink.ImageUrl = RootNodeImageUrl;
            }
            spanTag.Controls.Add(nodeLink);
            spanTag.RenderControl(output);
          }

          isRoot = false;
        }
      }
    }

    ///
    /// Determine if the node should be ignored.
    ///
    ///
    ///
    private bool IsIgnore(string nodeName)
    {
      bool isIgnore = false;
      string[] ignoreNodeNames;
      if(IgnoreNodeName != String.Empty)
      {
        char[] splitChar = new char[1];
        splitChar[0] = ',';
        ignoreNodeNames = IgnoreNodeName.Split(splitChar);

        foreach (string ignoreString in ignoreNodeNames)
        {
          if (Regex.IsMatch(nodeName, ignoreString, RegexOptions.IgnoreCase))
          {
            isIgnore = true;
            break;
          }
        }
      }
      return isIgnore;
    }
  }
}

23 comments September 28, 2006


 

September 2006
M T W T F S S
    Feb »
 123
45678910
11121314151617
18192021222324
252627282930  

Feeds

Archives

Blog Stats

ASP.NET

MOSS 2007

technology