tirsdag den 17. april 2012

Creating dynamic rendering anchors in Sitecore 6.5

When you work with sitecore like me, you know the great concept of renderings. It seems like the trend for new sites at the moment are a smaller amount of pages, but with many renderings on each page.

This is really cool, and very user freindly on Iphone, Ipads an other mobile devices.

But it also requires us to implement a navigation component, so that the user don't get lost on the page :-)
A way to do this, is to implement an anchor navigation bar.

Anchors of cause is Back to basis HTML, but implementing anchors along with the rendering stategy of sitecore is not straight forward.

In this post i will show you how it can be done. I will not be focusing on Jquery, animations etc... Only on the backend part related to sitecore.

The following code has been tested with sitecore 6.5. However it should work on sitecore 6.x.


So where do we start?

Well, first let's take a look at an outline of the this we are going to implement :

1. Adding an Achor Field to our rendering templates in sitecore
2. Building a simple Anchor component using a Sublayout.
3. Putting the Sitecore Rendering Pipelines into the trash.
4. Creating our own pipeline to handle all renderings.
5. Creating a menu component using a sublayout for navigation


1. Adding an achor Field to rendering templates in sitecore

Why do we need this? Well, we could skip this part if we wanted all renderings to have it's own Anchor.
But often that is not the case. So we need some way to define that a rendering should generate an anchor.

We do this by adding a "Anchor" field to the renderings Parameter Template.


With this field in place we are now able to specify if the rendering should generate an anchor or not as you will see later on.



2. Building a simple Anchor component

Why? Well, this component is the piece of HTML that will write the Anchor tag to the page when ever needed. Basicly it's just a sublayout with 3 lines of C# code.

Create a sublayout in sitecore, and add a Literal Control to the frontend. Call this literal 'lbanchor' or what ever you want.

Now add the following 3 lines of code to the PageLoad method of the control :
  
protected void Page_Load(object sender, EventArgs e)
        {
            NameValueCollection parameters = Sitecore.Web.WebUtil.ParseUrlParameters(((Sitecore.Web.UI.WebControls.Sublayout)this.Parent).Parameters);
            Regex r = new Regex("(?:[^a-z0-9]|(?<=['\"])s)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
            lbanchor.Text = "<a class='sectionLink' id=\"" + r.Replace(parameters["anchor"], string.Empty) +"\"></a>";
        }

This will extract the Anchor value from your rendering an create an Anchor tag on your page at load time.

As you will see later on, we need the ID of this sublayout item. Hardcoding ID's could be a way, but i prefer using a setting to store the value. So in the web.config file, under the sitecore settings section, add the following setting, and replace the value with the ID of your anchor control.

 <setting name="Anchors.RenderingID" value="{1C94D1AE-D73D-4492-8AC6-39C7493C4F28}">

To put this anchor component into play, we need to recreate the Renderings pipeline as i will show you next.


3/4. Replaceing the Sitecore Rendering Pipeline

This might be considered as a bit of a hack. But we need to do it, to make our Anchor component work.

First go into your web.config file.

Find <insertRenderings> section and remove the "AddRenderings" processor.(You might wanna wait until we are done creating our own processor before you delete it).



Nowinto Visual Studio and create a new Class.
This class we do everying that the standard sitecore processor does, but will also handle our anchors.

The complete code for this is posted below. You don't need to know what every single method does. The fun part is in the ExtractReferences Method, and you do need to know how that works.

Basicly what it does, is to find all renderings of a page, and add them to an array of rendering.
This is exactly what the sitecore processor does, but this custom processor also add's the Anchor rendering just above all rendering the told our code that they wanted it. (And we do this by checking the "Anchor" parameter field.

If a rendering wants us to put the Anchor rendering into the collection, just before the rendering itself, then we manually create a new Anchor rendering and specify all the needed information, including Sublayout ID (from the setting we created), placeholder etc...




using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Sitecore.Diagnostics;
using Sitecore;
using Sitecore.Pipelines.InsertRenderings;
using Sitecore.Web;
using Sitecore.Layouts;
using Sitecore.Data.Items;
using System.Xml;
using Sitecore.Globalization;
using Sitecore.Data.Fields;
using Sitecore.Data;
using System.Collections.Specialized;
using System.Text.RegularExpressions;
namespace RenderingExampleCode.Pipelines.Renderings
{
    public class AddRenderings : InsertRenderingsProcessor
    {
        /// <summary>
        /// Default Constructor
        /// </summary>
        public AddRenderings()
        {
        }

        /// <summary>
        /// Main Entry point of our custom Renderings processor
        /// </summary>
        /// <param name="args"></param>
        public override void Process(InsertRenderingsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (!args.HasRenderings && (args.ContextItem != null))
            {
                DeviceItem device = Context.Device;
                if (device != null)
                {
                    args.Renderings.AddRange(GetRenderings(device, true));
                    args.HasRenderings = args.Renderings.Count > 0;
                }
            }
        }
        public static RenderingReference[] ExtractReferences(XmlNode deviceNode, Language language, Database database)
        {
            Assert.ArgumentNotNull(deviceNode, "deviceNode");
            Assert.ArgumentNotNull(language, "language");
            Assert.ArgumentNotNull(database, "database");
            XmlNodeList list = deviceNode.SelectNodes("r");
            List<RenderingReference> referenceCollection = new List<RenderingReference>();
            for (int i = 0; i < list.Count; i++)
            {
                RenderingReference rr = new RenderingReference(list[i], language, database);
                var parameters = WebUtil.ParseQueryString(rr.Settings.Parameters);

                if (parameters != null && parameters["anchor"] == "1")
                {
                    RenderingDefinition renderingDefinition = new RenderingDefinition();
                    NameValueCollection renderingParameters = WebUtil.ParseUrlParameters(rr.Settings.Parameters);
                    renderingDefinition.ItemID = Sitecore.Configuration.Settings.GetSetting("Anchors.RenderingID");
                    renderingDefinition.Placeholder = rr.Settings.Placeholder;

                    string RenderingTitle = string.Empty;

                    foreach (string s in renderingParameters.AllKeys)
                    {
                        // Look for a parameter that contains something that would a title. This part you should
                        // redo with your own logic. I've stripped this example code a bit. To extract the title value
                        // i use some extension methods witch are not a part of this example code :-).
                        if (s.ToLower().Contains("title"))
                        {
                            RenderingTitle = renderingParameters[s];
                            break;
                        }
                    }

                    if (RenderingTitle != string.Empty)
                    {
                        renderingDefinition.Parameters = "Title=" + RenderingTitle;
                        referenceCollection.Add(new RenderingReference(renderingDefinition, language, database));
                    }
                }
                // Now our Anchor rendering has been added if the rendering told us to.
                // if 'anchor' field of this rendering had the value '1'.
                referenceCollection.Add(new RenderingReference(list[i], language, database));
            }
            return referenceCollection.ToArray();
        }
        public RenderingReference[] GetRenderings(DeviceItem device, bool checkLogin)
        {
            RenderingReference[] referenceArray;
            Assert.ArgumentNotNull(device, "device");
         
            XmlNode deviceNode = GetDeviceNode(device, true);
            if (deviceNode == null)
            {
                return new RenderingReference[0];
            }
            using (new LanguageSwitcher(WebUtil.GetCookieValue("shell", "lang", Context.Language.Name)))
            {
                referenceArray = ExtractReferences(deviceNode, Language.Current, Context.Item.Database);
            }
            if (checkLogin && !Context.User.Identity.IsAuthenticated)
            {
                UseLoginRenderings(referenceArray);
            }
            return referenceArray;
        }

        private XmlNode GetDeviceNode(DeviceItem device, bool requireLayout)
        {
            Assert.ArgumentNotNull(device, "device");
            return this.DoGetDeviceNode(device, requireLayout, string.Empty);
        }
        private XmlNode DoGetDeviceNode(DeviceItem device, bool requireLayout, string visited)
        {
            Assert.ArgumentNotNull(device, "device");
            Assert.ArgumentNotNull(visited, "visited");
            if (visited.IndexOf(device.ID.ToString()) >= 0)
            {
                return null;
            }
            XmlNode deviceNode = this.DoGetDeviceNode(device);
            if ((deviceNode != null) && (!requireLayout || !ID.IsNullOrEmpty(LayoutField.ExtractLayoutID(deviceNode))))
            {
                return deviceNode;
            }
            DeviceItem fallbackDevice = device.FallbackDevice;
            if (fallbackDevice == null)
            {
                return null;
            }
            visited = visited + device.ID.ToString();
            return this.DoGetDeviceNode(fallbackDevice, requireLayout, visited);
        }
        private XmlNode DoGetDeviceNode(DeviceItem device)
        {
            Assert.ArgumentNotNull(device, "device");
            XmlNode node = DoGetDeviceNode(device, Context.Item);
            if (node != null)
            {
                return node;
            }
            TemplateItem template = Context.Item.Template;
            if (template == null)
            {
                return null;
            }
            return DoGetDeviceNode(device, template.InnerItem);
        }
        private static XmlNode DoGetDeviceNode(DeviceItem device, Item item)
        {
            Assert.ArgumentNotNull(device, "device");
            Assert.ArgumentNotNull(item, "item");
            LayoutField field = item.Fields[FieldIDs.LayoutField];
            return field.GetDeviceNode(device);
        }
        private static void UseLoginRenderings(RenderingReference[] references)
        {
            Assert.ArgumentNotNull(references, "references");
            for (int i = 0; i < references.Length; i++)
            {
                RenderingReference reference = references[i];
                RenderingItem renderingItem = reference.RenderingItem;
                if (renderingItem != null)
                {
                    string str = renderingItem.InnerItem["login rendering"];
                    if (str.Length > 0)
                    {
                        Database database = renderingItem.Database;
                        ID id = ID.Parse(str);
                        Language language = renderingItem.InnerItem.Language;
                        RenderingItem item2 = database.Resources.Renderings[id, language];
                        if (item2 != null)
                        {
                            references[i] = new RenderingReference(item2) { Placeholder = reference.Placeholder };
                        }
                    }
                }
            }
        }
    }
}




Now the only thing we need to to, is to delete the standard sitecore processor from web.config and replace it with our own, using the namespace, class and assembly our processor is located in.


Now you could run the code and it would add anchors to all your sections where the "anchor" parameter field had the value "1" (Checked if using a checkbox field).


5. Creating a navigation component

So now that we have anchors on the page, we want to use them as well.

We could do that by creating an navigation menu component.
In this example i used a sublayout with a Repeater control.
Below you will find both the C# and Frontend Code.



C# Part :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Collections.Specialized;
using System.Text.RegularExpressions;

namespace RenderingExampleCode.layouts.Modules
{
    public partial class RightMenu : System.Web.UI.UserControl
    {
        private NameValueCollection _parameters;

        protected void Page_Load(object sender, EventArgs e)
        {
            List<string> values = new List<string>();
            foreach (Sitecore.Layouts.RenderingReference rr in Sitecore.Context.Page.Renderings)
            {
                try
                {
                    if (rr.RenderingID.ToString().ToLower() == Sitecore.Configuration.Settings.GetSetting("Anchors.RenderingID").ToLower())
                    {
                        _parameters = Sitecore.Web.WebUtil.ParseUrlParameters(rr.Settings.Parameters);
                        values.Add(_parameters[0]);
                    }
                }
                catch { }
            }
            if (values.Count > 0)
            {
                rightmenu.DataSource = values;
                rightmenu.DataBind();
            }
        }
        protected void rightmenu_ItemDataBound(object sender, RepeaterItemEventArgs e)
        {
            if (e.Item.ItemType == ListItemType.Header)
            {
                Literal lbpagename = (Literal)e.Item.FindControl("lbpagename");
                lbpagename.Text = "<a href=\"#\">" + SitecoreByValtech.Kernel.Xsl.XsltHelper.ItemTitle(Sitecore.Context.Item) + "</a>";
            }
            else if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
            {
                Literal lblink = (Literal)e.Item.FindControl("lblink");
                lblink.Text = "<a href=\"#" + ValidAnchorText((string)e.Item.DataItem) + "\">" + (string)e.Item.DataItem + "</a>";
            }
        }
        private static string ValidAnchorText(string input)
        {
            Regex r = new Regex("(?:[^a-z0-9]|(?<=['\"])s)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
            return r.Replace(input, String.Empty);
        }
    }
}




Frontend Part :


<div class="rightMenu">
    <asp:Repeater ID="rightmenu" runat="server" OnItemDataBound="rightmenu_ItemDataBound">
        <HeaderTemplate>
            <ul class="mainTab">
                <li><a href="#">
                    <asp:Literal ID="lbpagename" runat="server"></asp:Literal></a></li>
            </ul>
            <ul>
        </HeaderTemplate>
        <ItemTemplate>
            <li>
                <asp:Literal ID="lblink" runat="server"></asp:Literal></li>
        </ItemTemplate>
        <FooterTemplate>
            </ul>
        </FooterTemplate>
    </asp:Repeater>
</div>






I know that some of you might think that i am taking sitecore way to much apart, by completely replacing their rendering pipeline. Well maybe.

It's up to you to decide what your approch would be. But this solution gives the following benefits ;


  1. It can be implemented directly into an existing sitecore solution, without having to change on line of code in the existing source code for the site.
  2. It can be removed just as easy. 
  3. It will work on ALL new or existing renderings, as long as the rendering template has a checkbox field called "Anchor".
  4. This code does not change anything in your renderings inside sitecore. 
  5. The editor will never see the anchor renderings anywhere.

Feel free to comment and ask questions.

Best Regards
Lasse Rasch

Ingen kommentarer:

Send en kommentar