Home » 2015 » October

Monthly Archives: October 2015

Advertisements

Render a Custom General Link Field Attribute using a Custom Glass.Mapper Controller in Sitecore

In my previous posts — please be sure to read this post followed by this post and then this post before moving forward since you’ll need some context, and some of the code below is dependent on code in these previous posts — I showed two approaches for rendering a custom attribute — I called this attribute Tag in these posts and will continue to do so here — on a link set in a General Link field which is rendered using the Sitecore ORM Glass.Mapper.

In this post, I am going to share an approach on achieving the same but using a custom GlassController coupled with a class that implements the IGlassHtml interface — the code for this class can be found in this post.

First, we need a model to experiment with. I built the following class to serve as an example for this post:

using Sitecore.Data;

using Glass.Mapper.Sc.Configuration.Attributes;

using Sitecore.Sandbox.Glass.Mapper.Sc.Fields;

namespace Sitecore.Sandbox.Models
{
    public class SampleItemModel
    {
        [SitecoreId]
        ID ItemID { get; set; }

        public string Title { get; set; }

        public string Text { get; set; }

        [SitecoreField("Link One")]
        public TagLink LinkOne { get; set; }

        [SitecoreField("Link Two")]
        public TagLink LinkTwo { get; set; }
    }
}

Next, we need a ViewModel. I built the following as well to serve as an example for this post:

using System.Web.Mvc;

using Sitecore.Data;

using Glass.Mapper.Sc.Configuration.Attributes;

namespace Sitecore.Sandbox.Models.ViewModels
{
    public class SampleItemViewModel
    {
        public MvcHtmlString Title { get; set; }

        public MvcHtmlString Text { get; set; }

        public MvcHtmlString LinkOne { get; set; }

        public MvcHtmlString LinkTwo { get; set; }
    }
}

I then built the following subclass of Glass.Mapper.Sc.Web.Mvc.GlassController:

using System;
using System.Linq.Expressions;
using System.Web;
using System.Web.Mvc;

using Sitecore.Configuration;
using Sitecore.Diagnostics;

using Glass.Mapper.Sc;
using Glass.Mapper.Sc.Web.Mvc;
using Glass.Mapper.Sc.Web;
using GlassMapperSc = Glass.Mapper.Sc;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc.Controllers
{
    public abstract class SandboxGlassController : GlassController
    {
        private static IGlassHtmlFactory GlassHtmlFactory { get; set; }

        static SandboxGlassController()
        {
            GlassHtmlFactory = CreateGlassHtmlFactory();
        }

        public SandboxGlassController() 
            : this(GetContextFromHttp())
        {
        }

        protected SandboxGlassController(ISitecoreContext sitecoreContext) 
            : this(sitecoreContext, GlassHtmlFactory.CreateGlassHtml(sitecoreContext), new RenderingContextMvcWrapper(), null)
        {
        }

        public SandboxGlassController(ISitecoreContext sitecoreContext, IGlassHtml glassHtml, IRenderingContext renderingContextWrapper, HttpContextBase httpContext)
            : base(sitecoreContext, glassHtml, renderingContextWrapper, httpContext)
        {
        }

        protected static IGlassHtmlFactory CreateGlassHtmlFactory()
        {
            IGlassHtmlFactory factory = Factory.CreateObject("sandbox.Glass.Mvc/glassHtmlFactory", true) as IGlassHtmlFactory;
            Assert.IsNotNull(factory, "Be sure the configuration is correct in utilities/customAttributesAdder of your Sitecore configuration!");
            return factory;
        }

        protected static ISitecoreContext GetContextFromHttp()
        {
            try
            {
                return GlassMapperSc.SitecoreContext.GetFromHttpContext(null);
            }
            catch (Exception exception)
            {
                Log.Error("Failed to create SitecoreContext", exception, typeof(SandboxGlassController));
            }

            return null;
        }

        protected virtual MvcHtmlString Editable<T>(T model, Expression<Func<T, object>> field, object parameters = null)
        {
            return ConvertToMvcHtmlString(GlassHtml.Editable(model, field, parameters));
        }

        protected virtual MvcHtmlString RenderLink<T>(T model, Expression<Func<T, object>> link, object attributes = null, bool IsEditable = false)
        {
            return ConvertToMvcHtmlString(GlassHtml.RenderLink(model, link, attributes, IsEditable));
        }

        protected virtual MvcHtmlString ConvertToMvcHtmlString(string value)
        {
            return new MvcHtmlString(value);
        }
    }
}

I declared the above class as abstract since I don’t see how it could live on its own — none of its methods return ViewResult or ActionResult instances.

When an instance of the above class is created, an instance of a IGlassHtmlFactory — this is defined in my last post — is used to create an instance of a IGlassHtml which adds a Tag attribute and value to the link attributes when applicable. The Editable and RenderLink methods delegate to methods with the same signature on the IGlassHtml instance, and then transform the returned strings into MvcHtmlString instances via the ConvertToMvcHtmlString method so that the fields work in the Sitecore Experience Editor

I then built the following subclass of the above class for testing:

using System.Web.Mvc;

using Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc.Controllers;
using Sitecore.Sandbox.Models;
using Sitecore.Sandbox.Models.ViewModels;

namespace Sitecore.Sandbox.Web.Mvc.Controllers
{
    public class SampleItemController : SandboxGlassController
    {
        public ViewResult MainContent()
        {
            SampleItemModel model = SitecoreContext.GetCurrentItem<SampleItemModel>();
            if(model == null)
            {
                return View();
            }

            SampleItemViewModel viewModel = new SampleItemViewModel
            {
                Title = Editable(model, x => x.Title),
                Text = Editable(model, x => x.Text),
                LinkOne = RenderLink(model, x => x.LinkOne, null, true),
                LinkTwo = RenderLink(model, x => x.LinkTwo, null, true)
            };

            return View(viewModel);
        }
    }
}

The MainContent method gets an instance of a SampleItemModel from the current Item — in this example these are fields on the home Item — and transforms this into a SampleItemViewModel instance using the protected methods defined on its base class. The SampleItemViewModel instance is then sent to the View.

I then whipped up the following Razor View so we can see some data on the front-end:

@model Sitecore.Sandbox.Models.ViewModels.SampleItemViewModel

@if(Model == null)
{
    return;
}

<div id="Content">
    <div id="LeftContent">
    </div>
    <div id="CenterColumn">
        <div id="Header">
            <img src="/~/media/Default Website/sc_logo.png" id="scLogo" />
        </div>
        <h1 class="contentTitle">
            @Model.Title
        </h1>
        <div class="contentDescription">
            @Model.Text
            <div>
                @Model.LinkOne
            </div>
            <div>
                @Model.LinkTwo
            </div>
        </div>
    </div>
</div>

There isn’t much going on in the above Razor View — it’s just displaying data from the ViewModel.

I am going to omit how I wired-up the Controller above with a Controller rendering in Sitecore. If you don’t know how to wire-up Controllers with Controller renderings in Sitecore, please watch this video by Martina Welander.

Let’s see if this works (works on my machine 😉 ).

First, I ensured I had Tag atrributes set on my two General Link fields on my home Item in Sitecore:

tag-attributes-are-set-controller

Once I built and deployed everything, I navigated to my homepage Item, and saw the following rendered HTML:

tag-attribute-rendered-controller

As you can see, it worked. 😀

If you have any questions/comments/thoughts, please leave a comment.

Advertisements

A 2nd Approach to Render a Custom General Link Field Attribute in a Sitecore MVC View Rendering via Glass.Mapper

In my previous post, I shared an approach for customizing the Glass.Mapper Sitecore ORM to render a custom attribute on a link defined in a General Link field (I called this attribute Tag and will continue to do so in this post).

In this post, I will share a second approach — an approach that extends the “out of the box” Html Helper in Glass.

Note: be sure to read this post first followed by my last post before reading the current post — I am omitting code from both of these which is used here.

I first created a class that implements the Glass.Mapper.Sc.IGlassHtml interface:

using System;
using System.Collections.Specialized;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq.Expressions;

using Sitecore.Collections;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Diagnostics;

using Glass.Mapper.Sc;
using Glass.Mapper.Sc.Fields;
using Glass.Mapper.Sc.Web.Ui;
using Utilities = Glass.Mapper.Utilities;

using Sitecore.Sandbox.Glass.Mapper.Sc.Attributes;
using Sitecore.Sandbox.Glass.Mapper.Sc.Fields;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc
{
    public class SandboxGlassHtml : IGlassHtml
    {
        private ICustomAttributesAdder attributesAdder;
        private ICustomAttributesAdder AttributesAdder
        {
            get
            {
                if (attributesAdder == null)
                {
                    attributesAdder = GetCustomAttributesAdder();
                }

                return attributesAdder;
            }
        }

        private IGlassHtml InnerGlassHtml { get; set; }

        public ISitecoreContext SitecoreContext
        {
            get
            {
                return InnerGlassHtml.SitecoreContext;
            }
        }

        public SandboxGlassHtml(ISitecoreContext sitecoreContext)
            : this(new GlassHtml(sitecoreContext))
        {
        }

        protected SandboxGlassHtml(IGlassHtml innerGlassHtml)
        {
            SetInnerGlassHtml(innerGlassHtml);
        }

        private void SetInnerGlassHtml(IGlassHtml innerGlassHtml)
        {
            Assert.ArgumentNotNull(innerGlassHtml, "innerGlassHtml");
            InnerGlassHtml = innerGlassHtml;
        }

        public virtual RenderingResult BeginRenderLink<T>(T model, Expression<Func<T, object>> field, TextWriter writer, object attributes = null, bool isEditable = false)
        {
            object attributesModified = AttributesAdder.AddTagAttribute(model, field, attributes);
            return InnerGlassHtml.BeginRenderLink(model, field, writer, attributesModified, isEditable);
        }

        public virtual string Editable<T>(T target, Expression<Func<T, object>> field, object parameters = null)
        {
            return InnerGlassHtml.Editable(target, field, parameters);
        }

        public virtual string Editable<T>(T target, Expression<Func<T, object>> field, Expression<Func<T, string>> standardOutput, object parameters = null)
        {
            return InnerGlassHtml.Editable(target, field, standardOutput, parameters);
        }

        public virtual GlassEditFrame EditFrame(string buttons, string path = null, TextWriter output = null)
        {
            return InnerGlassHtml.EditFrame(buttons, path, output);
        }

        public virtual GlassEditFrame EditFrame<T>(T model, string title = null, TextWriter output = null, params Expression<Func<T, object>>[] fields) where T : class
        {
            return InnerGlassHtml.EditFrame(model, title, output, fields);
        }

        public virtual T GetRenderingParameters<T>(NameValueCollection parameters) where T : class
        {
            return InnerGlassHtml.GetRenderingParameters<T>(parameters);
        }

        public virtual T GetRenderingParameters<T>(string parameters) where T : class
        {
            return InnerGlassHtml.GetRenderingParameters<T>(parameters);
        }

        public virtual T GetRenderingParameters<T>(NameValueCollection parameters, ID renderParametersTemplateId) where T : class
        {
            return InnerGlassHtml.GetRenderingParameters<T>(parameters, renderParametersTemplateId);
        }

        public virtual T GetRenderingParameters<T>(string parameters, ID renderParametersTemplateId) where T : class
        {
            return InnerGlassHtml.GetRenderingParameters<T>(parameters, renderParametersTemplateId);
        }

        public virtual string RenderImage<T>(T model, Expression<Func<T, object>> field, object parameters = null, bool isEditable = false, bool outputHeightWidth = false)
        {
            return InnerGlassHtml.RenderImage(model, field, parameters, isEditable, outputHeightWidth);
        }

        public virtual string RenderLink<T>(T model, Expression<Func<T, object>> field, object attributes = null, bool isEditable = false, string contents = null)
        {
            object attributesModified = AttributesAdder.AddTagAttribute(model, field, attributes);
            return InnerGlassHtml.RenderLink(model, field, attributesModified, isEditable, contents);
        }

        public virtual string ProtectMediaUrl(string url)
        {
            return InnerGlassHtml.ProtectMediaUrl(url);
        }

        protected virtual ICustomAttributesAdder GetCustomAttributesAdder()
        {
            return CustomAttributesAdder.Current;
        }
    }
}

In the above class, I’m using the Decorator Pattern — another Glass.Mapper.Sc.IGlassHtml instance (this is set to an instance of Glass.Mapper.Sc.GlassHtml by default — have a look at the public constructor above) is passed to the class instance and stored in a private property. Every interface-defined method implemented in this class delegates to the inner-IGlassHtml instance.

Since I’m only targeting links in this solution, I utilize a CustomAttributesAdder instance — this is a Singleton which I shared in my last post which is defined in the Sitecore configuration file further down in this post — in both RenderLink methods. The CustomAttributesAdder instance adds the Tag attribute name and value to the attributes collection when applicable. The modified/unmodified attributes collection is then passed to the RenderLink method with the same signature on the inner Glass.Mapper.Sc.IGlassHtml instance.

Now, we need a way to instantiate the above class. I decided to create the following interface for classes that create instances of classes that implement the Glass.Mapper.Sc.IGlassHtml interface:

using Glass.Mapper.Sc;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc
{
    public interface IGlassHtmlFactory
    {
        IGlassHtml CreateGlassHtml(ISitecoreContext sitecoreContext);
    }
}

I then built the following class which creates an instance of the SandboxGlassHtml class defined above:

using Sitecore.Diagnostics;

using Glass.Mapper.Sc;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc
{
    public class SandboxGlassHtmlFactory : IGlassHtmlFactory
    {
        public IGlassHtml CreateGlassHtml(ISitecoreContext sitecoreContext)
        {
            Assert.ArgumentNotNull(sitecoreContext, "sitecoreContext");
            return new SandboxGlassHtml(sitecoreContext);
        }
    }
}

There isn’t much going on in the the class above exception object instantiation — the above is an example of the Factory method pattern for those who are curious.

Now, we need an extension method on the ASP.NET MVC HtmlHelper instance used in our Razor views in order to leverage the custom Glass.Mapper.Sc.IGlassHtml class defined above:

using System.Web.Mvc;

using Sitecore.Configuration;
using Sitecore.Diagnostics;

using Glass.Mapper.Sc;
using Glass.Mapper.Sc.Web.Mvc;

using Sitecore.Sandbox.Glass.Mapper.Sc.Attributes;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc
{
    public static class SandboxHtmlHelperExtensions
    {
        private static IGlassHtmlFactory GlassHtmlFactory { get; set; }

        static SandboxHtmlHelperExtensions()
        {
            GlassHtmlFactory = CreateGlassHtmlFactory();
        }

        public static GlassHtmlMvc<T> SandboxGlass<T>(this HtmlHelper<T> htmlHelper)
        {
            IGlassHtml glassHtml = GlassHtmlFactory.CreateGlassHtml(SitecoreContext.GetFromHttpContext(null));
            Assert.IsNotNull(glassHtml, "glassHtml cannot be null!");
            return new GlassHtmlMvc<T>(glassHtml, htmlHelper.ViewContext.Writer, htmlHelper.ViewData.Model);
        }

        private static IGlassHtmlFactory CreateGlassHtmlFactory()
        {
            IGlassHtmlFactory factory = Factory.CreateObject("sandbox.Glass.Mvc/glassHtmlFactory", true) as IGlassHtmlFactory;
            Assert.IsNotNull(factory, "Be sure the configuration is correct in utilities/customAttributesAdder of your Sitecore configuration!");
            return factory;
        }
    }
}

In the SandboxGlass method above, we instantiate an instance of the IGlassHtmlFactory which is defined in Sitecore configuration (see the patch configuration file below) and use it to create an instance of whatever Glass.Mapper.Sc.IGlassHtml it is tasked to create (in our case here it’s an instance of the SandboxGlassHtml class defined above). This is then passed to a newly created instance of Glass.Mapper.Sc.Web.Mvc.GlassHtmlMvc.

I then glued all the pieces together using the following Sitecore patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <controlSources>
      <source mode="on" namespace="Sitecore.Sandbox.Shell.Applications.ContentEditor" assembly="Sitecore.Sandbox" prefix="sandbox-content"/>
    </controlSources>
    <fieldTypes>
      <fieldType name="General Link">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
      </fieldType>
      <fieldType name="General Link with Search">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
      </fieldType>
      <fieldType name="link">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
        </fieldType>
    </fieldTypes>
    <pipelines>
      <dialogInfo>
        <processor type="Sitecore.Sandbox.Pipelines.DialogInfo.SetDialogInfo, Sitecore.Sandbox">
          <ParameterNameAttributeName>name</ParameterNameAttributeName>
          <ParameterValueAttributeName>value</ParameterValueAttributeName>
          <Message>contentlink:externallink</Message>
          <Url>/sitecore/shell/Applications/Dialogs/External link.aspx</Url>
          <parameters hint="raw:AddParameter">
            <parameter name="height" value="300" />
          </parameters>
        </processor>
      </dialogInfo>
      <renderField>
        <processor patch:after="processor[@type='Sitecore.Pipelines.RenderField.GetInternalLinkFieldValue, Sitecore.Kernel']" 
                   type="Sitecore.Sandbox.Pipelines.RenderField.SetTagAttributeOnLink, Sitecore.Sandbox">
          <TagXmlAttributeName>tag</TagXmlAttributeName>
          <TagAttributeName>tag</TagAttributeName>
          <BeginningHtml>&lt;a </BeginningHtml>
        </processor>  
      </renderField>
    </pipelines>
    <sandbox.Glass.Mvc>
      <customAttributesAdder type="Sitecore.Sandbox.Glass.Mapper.Sc.Attributes.CustomAttributesAdder, Sitecore.Sandbox" singleInstance="true" />
      <glassHtmlFactory type="Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc.SandboxGlassHtmlFactory, Sitecore.Sandbox" singleInstance="true" />
    </sandbox.Glass.Mvc>
  </sitecore>
</configuration>

Let’s see if this works.

For testing, I created the following Razor view — notice how I’m using the Html Helper instead of using the methods on the class the Razor view inherits from:

@inherits Glass.Mapper.Sc.Web.Mvc.GlassView<Sitecore.Sandbox.Models.ViewModels.ISampleItem>
@using Glass.Mapper.Sc.Web.Mvc
@using Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc

<div id="Content">
    <div id="LeftContent">
    </div>
    <div id="CenterColumn">
        <div id="Header">
            <img src="/~/media/Default Website/sc_logo.png" id="scLogo" />
        </div>
        <h1 class="contentTitle">
            @Html.SandboxGlass().Editable(x => x.Title)
        </h1>
        <div class="contentDescription">
            @Html.SandboxGlass().Editable(x => x.Text)
            <div>
                @Html.SandboxGlass().RenderLink(x => x.LinkOne)
            </div>
            <div>
                @Html.SandboxGlass().RenderLink(x => x.LinkTwo)
            </div>
        </div>
    </div>
</div>

After building and deploying everything above, I made sure I had some tags defined on some General Link fields on my home Item in Sitecore:

tag-attributes-raw-values

I then navigated to my homepage; looked at the rendered HTML; and saw the following:

tag-attributes-rendered-SandboxGlassHtml

As you can see it worked. 🙂

If you have any questions/comments/thoughts on this, please share in a comment.

Until next time, be sure to:

sitecore-all-the-things

😀

One Approach to Render a Custom General Link Field Attribute in a Sitecore MVC View Rendering via Glass.Mapper

In my previous post, I shared a way to add a custom attribute to the General Link field in Sitecore — in that post I called this attribute “Tag” and will continue to do so here — and also showed how to render it on the front-end using the Sitecore Link field control.

You might have been asking yourself when reading that last post “Mike, how would I go about getting this to work in the Sitecore ORM Glass.Mapper?” (well, actually, I planted a seed in that post that I was going to write another post on how to get this to work in Glass.Mapper so you might not have been asking yourself that at all but instead were thinking “Mike, just get on with it!”).

In this post, I am going to show you one approach on how to tweak Glass to render a Tag attribute in the rendered markup of a General Link field (I’m not going reiterate the bits on how to customize the General Link field as I had done in my previous post, so you might want to have a read of that first before reading this post).

I first created a custom Sitecore.Data.Fields.LinkField class:

using Sitecore.Data.Fields;

namespace Sitecore.Sandbox.Data.Fields
{
    public class TagLinkField : LinkField
    {
        public TagLinkField(Field innerField)
            : base(innerField)
        {
        }

        public TagLinkField(Field innerField, string runtimeValue)
            : base(innerField, runtimeValue)
        {
        }

        public string Tag
        {
            get
            {
                return GetAttribute("tag");
            }
            set
            {
                this.SetAttribute("tag", value);
            }
        }   
    }
}

An instance of this class will magically create a XML representation of itself when saving to the General Link field, and will also parse the attributes that are defined in the XML.

Next, we need a Glass.Mapper field like Glass.Mapper.Sc.Fields.Link but with an additional property for the Tag value. This sound like an opportune time to subclass Glass.Mapper.Sc.Fields.Link and add a new property to hold the Tag value 😉 :

using Glass.Mapper.Sc.Fields;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Fields
{
    public class TagLink : Link
    {
        public string Tag { get; set; }
    }
}

There’s nothing much in the above class except for an additional property for the Tag attribute value.

I then built the following Glass.Mapper.Sc.DataMappers.AbstractSitecoreFieldMapper for the TagLink:

using System;

using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Links;
using Sitecore.Resources.Media;

using Glass.Mapper;
using Glass.Mapper.Sc;
using Glass.Mapper.Sc.Configuration;
using Glass.Mapper.Sc.DataMappers;
using Glass.Mapper.Sc.Fields;
using Utilities = Glass.Mapper.Sc.Utilities;

using Sitecore.Sandbox.Data.Fields;
using Sitecore.Sandbox.Glass.Mapper.Sc.Fields;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.DataMappers
{
    public class SitecoreFieldTagLinkMapper : AbstractSitecoreFieldMapper
    {
        private AbstractSitecoreFieldMapper InnerLinkMapper { get; set;}

        public SitecoreFieldTagLinkMapper()
            : this(new SitecoreFieldLinkMapper(), typeof(TagLink))
        {
        }

        public SitecoreFieldTagLinkMapper(AbstractSitecoreFieldMapper innerLinkMapper, Type linkType)
            : base(linkType)
        {
            SetInnerLinkMapper(innerLinkMapper);
        }

        private void SetInnerLinkMapper(AbstractSitecoreFieldMapper innerLinkMapper)
        {
            Assert.ArgumentNotNull(innerLinkMapper, "innerLinkMapper");
            InnerLinkMapper = innerLinkMapper;
        }
        
        public override string SetFieldValue(object value, SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
        {
            return InnerLinkMapper.SetFieldValue(value, config, context);
        }
        
        public override object GetFieldValue(string fieldValue, SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
        {
            return InnerLinkMapper.GetFieldValue(fieldValue, config, context);
        }

        public override object GetField(Field field, SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
        {
            if (field == null || field.Value.Trim().IsNullOrEmpty())
            {
                return null;
            }

            TagLink link = new TagLink();
            TagLinkField linkField = new TagLinkField(field);
            link.Anchor = linkField.Anchor;
            link.Class = linkField.Class;
            link.Text = linkField.Text;
            link.Title = linkField.Title;
            link.Target = linkField.Target;
            link.Query = linkField.QueryString;
            link.Tag = linkField.Tag;

            switch (linkField.LinkType)
            {
                case "anchor":
                    link.Url = linkField.Anchor;
                    link.Type = LinkType.Anchor;
                    break;
                case "external":
                    link.Url = linkField.Url;
                    link.Type = LinkType.External;
                    break;
                case "mailto":
                    link.Url = linkField.Url;
                    link.Type = LinkType.MailTo;
                    break;
                case "javascript":
                    link.Url = linkField.Url;
                    link.Type = LinkType.JavaScript;
                    break;
                case "media":
                    if (linkField.TargetItem == null)
                        link.Url = string.Empty;
                    else
                    {
                        global::Sitecore.Data.Items.MediaItem media =
                            new global::Sitecore.Data.Items.MediaItem(linkField.TargetItem);
                        link.Url = global::Sitecore.Resources.Media.MediaManager.GetMediaUrl(media);
                    }
                    link.Type = LinkType.Media;
                    link.TargetId = linkField.TargetID.Guid;
                    break;
                case "internal":
                    var urlOptions = Utilities.CreateUrlOptions(config.UrlOptions);
                    link.Url = linkField.TargetItem == null ? string.Empty : LinkManager.GetItemUrl(linkField.TargetItem, urlOptions);
                    link.Type = LinkType.Internal;
                    link.TargetId = linkField.TargetID.Guid;
                    link.Text = linkField.Text.IsNullOrEmpty() ? (linkField.TargetItem == null ? string.Empty : linkField.TargetItem.DisplayName) : linkField.Text;
                    break;
                default:
                    return null;
            }

            return link;
        }

        public override void SetField(Field field, object value, SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
        {
            if (field == null)
            {
                return;
            }

            TagLink link = value as TagLink;
            if(link == null)
            {
                return;
            }

            Item item = field.Item;
            TagLinkField linkField = new TagLinkField(field);
            if (link == null || link.Type == LinkType.NotSet)
            {
                linkField.Clear();
                return;
            }
            
            switch (link.Type)
            {
                case LinkType.Internal:
                    linkField.LinkType = "internal";
                    if (linkField.TargetID.Guid != link.TargetId)
                    {
                        if (link.TargetId == Guid.Empty)
                        {
                            ItemLink iLink = new ItemLink(item.Database.Name, item.ID, linkField.InnerField.ID, linkField.TargetItem.Database.Name, linkField.TargetID, linkField.TargetItem.Paths.FullPath);
                            linkField.RemoveLink(iLink);
                        }
                        else
                        {
                            ID newId = new ID(link.TargetId);
                            Item target = item.Database.GetItem(newId);
                            if (target != null)
                            {
                                linkField.TargetID = newId;
                                ItemLink nLink = new ItemLink(item.Database.Name, item.ID, linkField.InnerField.ID, target.Database.Name, target.ID, target.Paths.FullPath);
                                linkField.UpdateLink(nLink);
                                linkField.Url = LinkManager.GetItemUrl(target);
                            }
                            else throw new Exception(String.Format("No item with ID {0}. Can not update Link linkField", newId));
                        }

                    }
                    break;
                case LinkType.Media:
                    linkField.LinkType = "media";
                    if (linkField.TargetID.Guid != link.TargetId)
                    {
                        if (link.TargetId == Guid.Empty)
                        {
                            ItemLink iLink = new ItemLink(item.Database.Name, item.ID, linkField.InnerField.ID, linkField.TargetItem.Database.Name, linkField.TargetID, linkField.TargetItem.Paths.FullPath);
                            linkField.RemoveLink(iLink);
                        }
                        else
                        {
                            ID newId = new ID(link.TargetId);
                            Item target = item.Database.GetItem(newId);

                            if (target != null)
                            {
                                MediaItem media = new MediaItem(target);

                                linkField.TargetID = newId;
                                ItemLink nLink = new ItemLink(item.Database.Name, item.ID, linkField.InnerField.ID, target.Database.Name, target.ID, target.Paths.FullPath);
                                linkField.UpdateLink(nLink);
                                linkField.Url = MediaManager.GetMediaUrl(media);
                            }
                            else throw new Exception(String.Format("No item with ID {0}. Can not update Link linkField", newId));
                        }
                    }
                    break;
                case LinkType.External:
                    linkField.LinkType = "external";
                    linkField.Url = link.Url;
                    break;
                case LinkType.Anchor:
                    linkField.LinkType = "anchor";
                    linkField.Url = link.Anchor;
                    break;
                case LinkType.MailTo:
                    linkField.LinkType = "mailto";
                    linkField.Url = link.Url;
                    break;
                case LinkType.JavaScript:
                    linkField.LinkType = "javascript";
                    linkField.Url = link.Url;
                    break;
            }

            if (!link.Anchor.IsNullOrEmpty())
            {
                linkField.Anchor = link.Anchor;
            }
                
            if (!link.Class.IsNullOrEmpty())
            {
                linkField.Class = link.Class;
            }
                
            if (!link.Text.IsNullOrEmpty())
            {
                linkField.Text = link.Text;
            }
                
            if (!link.Title.IsNullOrEmpty())
            {
                linkField.Title = link.Title;
            }
                
            if (!link.Query.IsNullOrEmpty())
            {
                linkField.QueryString = link.Query;
            }
                
            if (!link.Target.IsNullOrEmpty())
            {
                linkField.Target = link.Target;
            }

            if (!link.Tag.IsNullOrEmpty())
            {
                linkField.Tag = link.Tag;
            }
        }
    }
}

Most of the code in the GetField and SetField methods above are taken from the same methods in Glass.Mapper.Sc.DataMappers.SitecoreFieldLinkMapper except for the additional lines for the TagLink.

In both methods a TagLinkField instance is created so that we can get the Tag value from the field.

The follow class is used by an <initialize> pipeline processor that configures Glass on Sitecore application start:

using Glass.Mapper.Configuration;
using Glass.Mapper.IoC;
using Glass.Mapper.Maps;
using Glass.Mapper.Sc;
using Glass.Mapper.Sc.IoC;
using Sitecore.Sandbox.DI;
using Sitecore.Sandbox.Glass.Mapper.Sc.DataMappers;
using IDependencyResolver = Glass.Mapper.Sc.IoC.IDependencyResolver;

namespace Sitecore.Sandbox.Web.Mvc.App_Start
{
    public static class GlassMapperScCustom
    {
		public static IDependencyResolver CreateResolver(){
			var config = new Config();
            DependencyResolver dependencyResolver = new DependencyResolver(config);
            AddDataMappers(dependencyResolver);
            return dependencyResolver;
		}

        private static void AddDataMappers(DependencyResolver dependencyResolver)
        {
            if(dependencyResolver == null)
            {
                return;
            }
            
            dependencyResolver.DataMapperFactory.Replace(15, () => new SitecoreFieldTagLinkMapper());
        }

		public static IConfigurationLoader[] GlassLoaders(){			
			
			/* USE THIS AREA TO ADD FLUENT CONFIGURATION LOADERS
             * 
             * If you are using Attribute Configuration or automapping/on-demand mapping you don't need to do anything!
             * 
             */

			return new IConfigurationLoader[]{};
		}
		public static void PostLoad(){
			//Remove the comments to activate CodeFist
			/* CODE FIRST START
            var dbs = Sitecore.Configuration.Factory.GetDatabases();
            foreach (var db in dbs)
            {
                var provider = db.GetDataProviders().FirstOrDefault(x => x is GlassDataProvider) as GlassDataProvider;
                if (provider != null)
                {
                    using (new SecurityDisabler())
                    {
                        provider.Initialise(db);
                    }
                }
            }
             * CODE FIRST END
             */
		}
		public static void AddMaps(IConfigFactory<IGlassMap> mapsConfigFactory)
        {
			// Add maps here
            ContainerManager containerManager = new ContainerManager();
            foreach (var map in containerManager.Container.GetAllInstances<IGlassMap>())
            {
                mapsConfigFactory.Add(() => map);
            }
        }
    }
}

I added the AddDataMappers method to it. This method replaces the “out of the box” SitecoreFieldLinkMapper with a SitecoreFieldTagLinkMapper instance — the “out of the box” SitecoreFieldLinkMapper lives in the 15th place in the index (I determined this using .NET Reflector on one of the Glass.Mapper assemblies).

Now that the above things are squared away, we need a way to add the Tag attribute with its value to the attributes collection that is passed to Glass so that it can transform this into rendered HTML. I decided to define an interface for classes that do that:

using System;
using System.Linq.Expressions;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Attributes
{
    public interface ICustomAttributesAdder
    {
        object AddTagAttribute<T>(T model, Expression<Func<T, object>> field, object attributes);
    }
}

Classes that implement the above interface will take in a Glass Model instance, the field we are rendering, and the existing collection of attributes that are to be rendered by Glass.

The following class implements the above interface:

using System;
using System.Collections.Specialized;
using System.Linq.Expressions;

using Sitecore.Configuration;
using Sitecore.Diagnostics;

using Glass.Mapper.Sc;

using Sitecore.Sandbox.Glass.Mapper.Sc.Fields;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Attributes
{
    public class CustomAttributesAdder : ICustomAttributesAdder
    {
        private static readonly Lazy<ICustomAttributesAdder> current = new Lazy<ICustomAttributesAdder>(() => { return GetCustomAttributesAdder(); });

        public static ICustomAttributesAdder Current
        {
            get
            {
                return current.Value;
            }
        }

        public CustomAttributesAdder()
        {
        }

        public virtual object AddTagAttribute<T>(T model, Expression<Func<T, object>> field, object attributes)
        {
            TagLink tagLink = field.Compile()(model) as TagLink;
            if (tagLink == null || string.IsNullOrWhiteSpace(tagLink.Tag))
            {
                return attributes;
            }

            NameValueCollection attributesCollection;
            if (attributes is NameValueCollection)
            {
                attributesCollection = attributes as NameValueCollection;
            }
            else
            {
                attributesCollection = Utilities.GetPropertiesCollection(attributes, true, true);
            }

            attributesCollection.Add("tag", tagLink.Tag);
            return attributesCollection;
        }

        private static ICustomAttributesAdder GetCustomAttributesAdder()
        {
            ICustomAttributesAdder adder = Factory.CreateObject("sandbox.Glass.Mvc/customAttributesAdder", true) as ICustomAttributesAdder;
            Assert.IsNotNull(adder, "Be sure the configuration for CustomAttributesAdder is correct in utilities/customAttributesAdder of your Sitecore configuration!");
            return adder;
        }
    }
}

The AddTagAttribute method above first determines if the field passed to it is a TagLink. If it’s not, it just returns the attribute collection unaltered.

The method also determines if there is a Tag value. If there is no Tag value, it just returns the attribute collection “as is” since we don’t want to render an attribute with an empty value.

If the field is a TagLink and there is a Tag value, the method adds the Tag attribute name and value into the attributes collection that was passed to it, and then returns the modified NameValueCollection instance.

I decided to use the Singleton Pattern for the above class — the type of the class is defined in Sitecore configuration (see the patch configuration file further down in this post — since I am going to reuse it in my next post where I’ll show another approach on rendering a Tag attribute using Glass.Mapper, and I had built both approaches simultaneously (I decided to break these into separate blog posts since this one by itself will already be quite long).

Next, I built a new subclass of Glass.Mapper.Sc.Web.Mvc.GlassView so that I can intercept attributes collection passed to the RenderLink methods on the “out of the box” Glass.Mapper.Sc.Web.Mvc.GlassView (our Razor views will have to inherit from the class below in order for everything to work):

using System;
using System.Collections.Specialized;
using System.Linq.Expressions;
using System.Web;

using Sitecore.Configuration;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Glass.Mapper.Sc.Attributes;
using Sitecore.Sandbox.Glass.Mapper.Sc.Fields;

using Glass.Mapper.Sc;
using Glass.Mapper.Sc.Fields;
using Glass.Mapper.Sc.Web.Mvc;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc
{
    public abstract class SandboxGlassView<TModel> : GlassView<TModel> where TModel : class
    {
        private ICustomAttributesAdder attributesAdder;
        private ICustomAttributesAdder AttributesAdder 
        { 
            get
            {
                if (attributesAdder == null)
                {
                    attributesAdder = GetCustomAttributesAdder();
                }

                return attributesAdder;
            }
        }

        public override RenderingResult BeginRenderLink<T>(T model, Expression<Func<T, object>> field, object attributes = null, bool isEditable = false)
        {
            object attributesModified = AttributesAdder.AddTagAttribute(model, field, attributes);
            return base.BeginRenderLink<T>(model, field, attributesModified, isEditable);
        }

        public override HtmlString RenderLink(Expression<Func<TModel, object>> field, object attributes = null, bool isEditable = false, string contents = null)
        {
            object attributesModified = AttributesAdder.AddTagAttribute(Model, field, attributes);
            return base.RenderLink(field, attributesModified, isEditable, contents);
        }

        public override HtmlString RenderLink<T>(T model, Expression<Func<T, object>> field, object attributes = null, bool isEditable = false, string contents = null)
        {
            object attributesModified = AttributesAdder.AddTagAttribute(model, field, attributes);
            return base.RenderLink<T>(model, field, attributesModified, isEditable, contents);
        }

        protected virtual ICustomAttributesAdder GetCustomAttributesAdder()
        {
            return CustomAttributesAdder.Current;
        }
    }
}

I used the CustomAttributesAdder Singleton instance to add the Tag attribute name and value into the passed attributes collection, and then pass it on to the base class to do its magic.

I then strung everything together using the following Sitecore patch configuration file (Note: lots of stuff in this configuration file come from my previous post so I advise having a look at it):

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <controlSources>
      <source mode="on" namespace="Sitecore.Sandbox.Shell.Applications.ContentEditor" assembly="Sitecore.Sandbox" prefix="sandbox-content"/>
    </controlSources>
    <fieldTypes>
      <fieldType name="General Link">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
      </fieldType>
      <fieldType name="General Link with Search">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
      </fieldType>
      <fieldType name="link">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
        </fieldType>
    </fieldTypes>
    <pipelines>
      <dialogInfo>
        <processor type="Sitecore.Sandbox.Pipelines.DialogInfo.SetDialogInfo, Sitecore.Sandbox">
          <ParameterNameAttributeName>name</ParameterNameAttributeName>
          <ParameterValueAttributeName>value</ParameterValueAttributeName>
          <Message>contentlink:externallink</Message>
          <Url>/sitecore/shell/Applications/Dialogs/External link.aspx</Url>
          <parameters hint="raw:AddParameter">
            <parameter name="height" value="300" />
          </parameters>
        </processor>
      </dialogInfo>
      <renderField>
        <processor patch:after="processor[@type='Sitecore.Pipelines.RenderField.GetInternalLinkFieldValue, Sitecore.Kernel']" 
                   type="Sitecore.Sandbox.Pipelines.RenderField.SetTagAttributeOnLink, Sitecore.Sandbox">
          <TagXmlAttributeName>tag</TagXmlAttributeName>
          <TagAttributeName>tag</TagAttributeName>
          <BeginningHtml>&lt;a </BeginningHtml>
        </processor>  
      </renderField>
    </pipelines>
    <sandbox.Glass.Mvc>
      <customAttributesAdder type="Sitecore.Sandbox.Glass.Mapper.Sc.Attributes.CustomAttributesAdder, Sitecore.Sandbox" singleInstance="true" />
    </sandbox.Glass.Mvc>
  </sitecore>
</configuration>

Let’s see this in action!

For testing, I created the following interface for a model for my Sitecore instance’s Home Item (we are using fields defined on the Sample Item template):

using Glass.Mapper.Sc.Configuration.Attributes;

using Sitecore.Sandbox.Glass.Mapper.Sc.Fields;

namespace Sitecore.Sandbox.Models.ViewModels
{
    public interface ISampleItem
    {
        string Title { get; set; }

        string Text { get; set; }

        [SitecoreField("Link One")]
        TagLink LinkOne { get; set; }

        [SitecoreField("Link Two")]
        TagLink LinkTwo { get; set; }
    }
}

Model instances of the above interface will have two TagLink instances on them.

Next, I built the following Glass.Mapper.Sc.Maps.SitecoreGlassMap for my model interface defined above:

using Glass.Mapper.Sc.Maps;

using Sitecore.Sandbox.Models.ViewModels;

namespace Sitecore.Sandbox.Mappings.ViewModels.SampleItem
{
    public class SampleItemMap : SitecoreGlassMap<ISampleItem>
    {
        public override void Configure()
        {
            Map(x =>
            {
                x.AutoMap();
            });
        }
    }
}

Glass.Mapper will create an instance of the above class which will magically create a concrete instance of a class that implements the ISampleItem interface.

We need to plug the above into the front-end. I did this using the following Razor view:

@inherits Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc.SandboxGlassView<Sitecore.Sandbox.Models.ViewModels.ISampleItem>
@using Glass.Mapper.Sc.Web.Mvc
@using Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc

<div id="Content">
    <div id="LeftContent">
    </div>
    <div id="CenterColumn">
        <div id="Header">
            <img src="/~/media/Default Website/sc_logo.png" id="scLogo" />
        </div>
        <h1 class="contentTitle">
            @Editable(x => x.Title)
        </h1>
        <div class="contentDescription">
            @Editable(x => x.Text)
            <div>
                @RenderLink(x => x.LinkOne)
            </div>
            <div>
                @RenderLink(x => x.LinkTwo)
            </div>
        </div>
    </div>
</div>

The above Razor file inherits from SandboxGlassView so that it can access the RenderLink methods that were defined in the SandboxGlassView class.

I then ensured I had some tag attributes set on some General Link fields on my home Item (I kept these the same as my last blog post):

tag-attributes-raw-values

After doing a build and navigating to my homepage Item, I saw the following in the rendered HTML:

tag-attributes-rendered

As you can see, it worked magically. 🙂

magic

If you have any questions/comments/thoughts on the above, please share in a comment.

Also, I would like to thank Sitecore MVP Nat Mann for helping me on some of the bits above. Without your help Nat, there would be no solution and no blog post.

Until next time, keep on Sitecore-ing. 😀

Add a Custom Attribute to the General Link Field in Sitecore

In my current project, I needed to find a way to give content authors the ability to add a custom attribute — let’s call this custom attribute Tag for simplicity– to the “Insert Link” and “Insert External Link” dialogs of the General Link field (NOTE: the following solution does not use the “out of the box” SPEAK dialogs that ship with Sitecore 7.2 and up. This solution uses the older Sheer UI dialogs. Perhaps I will share a solution in the future on how to do the following using the newer SPEAK dialogs).

You might be asking why? Well, let’s imagine that there is some magical JavaScript code that puts a click event on links, and grabs the value of the tag attribute for reporting purposes — perhaps the JavaScript calls a service that captures this information.

In this post, I am going to share how I went about doing this minus the code I needed to add to get this to work in the Glass.Mapper ORM (I’m going to show you this code in my next blog post).

I first built the following custom LinkField class (this class is not used in this solution but will be used in my next blog post where I should how to integrate the functionality below in Glass.Mapper. I’m just setting the stage 😉 ):

using Sitecore.Data.Fields;

namespace Sitecore.Sandbox.Data.Fields
{
    public class TagLinkField : LinkField
    {
        public TagLinkField(Field innerField)
            : base(innerField)
        {
        }

        public TagLinkField(Field innerField, string runtimeValue)
            : base(innerField, runtimeValue)
        {
        }

        public string Tag
        {
            get
            {
                return GetAttribute("tag");
            }
            set
            {
                this.SetAttribute("tag", value);
            }
        }   
    }
}

The class above subclasses Sitecore.Data.Fields.Link (this lives in Sitecore.Kernel.dll) — this class represents a link in Sitecore — and added a new Tag property (this class will magically parse or save this value into the field’s underlying XML).

Next, I created the following Sheer UI form for a custom “Insert Link” dialog:

using System;
using System.Xml;
using System.Collections.Specialized;

using Sitecore;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Applications.Dialogs;
using Sitecore.Shell.Applications.Dialogs.InternalLink;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.Sheer;
using Sitecore.Xml;

namespace Sitecore.Sandbox.Shell.Applications.Dialogs.InternalLink
{
    public class TagInternalLinkForm : InternalLinkForm
    {
        private const string TagAttributeName = "tag";

        protected Edit Tag;

        private NameValueCollection customLinkAttributes;
        protected NameValueCollection CustomLinkAttributes 
        { 
            get
            {
                if(customLinkAttributes == null)
                {
                    customLinkAttributes = new NameValueCollection();
                    ParseLinkAttributes(GetLink());
                }

                return customLinkAttributes;
            }
        }

        protected override void OnLoad(EventArgs e)
        {
            Assert.ArgumentNotNull(e, "e");
            base.OnLoad(e);
            if (Context.ClientPage.IsEvent)
            {
                return;
            }
            
            LoadControls();
        }

        protected override void ParseLink(string link)
        {
            base.ParseLink(link);
            ParseLinkAttributes(link);
        }

        protected virtual void ParseLinkAttributes(string link)
        {
            Assert.ArgumentNotNull(link, "link");
            XmlDocument xmlDocument = XmlUtil.LoadXml(link);
            if (xmlDocument == null)
            {
                return;
            }

            XmlNode node = xmlDocument.SelectSingleNode("/link");
            if (node == null)
            {
                return;
            }

            CustomLinkAttributes[TagAttributeName] = XmlUtil.GetAttribute(TagAttributeName, node);
        }

        protected virtual void LoadControls()
        {
            string tagValue = CustomLinkAttributes[TagAttributeName];
            if (!string.IsNullOrWhiteSpace(tagValue))
            {
                Tag.Value = tagValue;
            }
        }

        protected override void OnOK(object sender, EventArgs args)
        {
            Assert.ArgumentNotNull(sender, "sender");
            Assert.ArgumentNotNull(args, "args");
            Item selectionItem = Treeview.GetSelectionItem();
            if (selectionItem == null)
            {
                Context.ClientPage.ClientResponse.Alert("Select an item.");
            }
            else
            {
                string attributeFromValue = LinkForm.GetLinkTargetAttributeFromValue(this.Target.Value, this.CustomTarget.Value);
                string queryString = this.Querystring.Value;
                if (queryString.StartsWith("?", StringComparison.InvariantCulture))
                    queryString = queryString.Substring(1);

                Packet packet = new Packet("link", new string[0]);
                LinkForm.SetAttribute(packet, "text", (Control)Text);
                LinkForm.SetAttribute(packet, "linktype", "internal");
                LinkForm.SetAttribute(packet, "anchor", (Control)Anchor);
                LinkForm.SetAttribute(packet, "title", (Control)Title);
                LinkForm.SetAttribute(packet, "class", (Control)Class);
                LinkForm.SetAttribute(packet, "querystring", queryString);
                LinkForm.SetAttribute(packet, "target", attributeFromValue);
                LinkForm.SetAttribute(packet, "id", selectionItem.ID.ToString());

                TrimEditControl(Tag);
                LinkForm.SetAttribute(packet, TagAttributeName, (Control)Tag);

                Assert.IsTrue(!string.IsNullOrEmpty(selectionItem.ID.ToString()) && ID.IsID(selectionItem.ID.ToString()), "ID doesn't exist.");
                SheerResponse.SetDialogValue(packet.OuterXml);
                SheerResponse.CloseWindow();
            }
        }

        protected virtual void TrimEditControl(Edit control)
        {
            if(control == null || string.IsNullOrEmpty(control.Value))
            {
                return;
            }

            control.Value = control.Value.Trim();
        }
    }
}

The OnLoad method invokes its base class’ OnLoad method — the base class’ OnLoad method loads values from the field’s XML into the Edit controls on the form — and also parses the value from the tag XML attribute and places it into the Tag Edit control.

The ParseLink method above is where values from the field’s XML are extracted — these are extracted from the XML attributes of the field. The ParseLink method delegates to the ParseLinkAttributes method which extracts the value from the tag attribute.

The OnOK method is where values from the Edit controls are extract and passed to a class instance that generates XML for the field. I could not call the base class’ OnOK method since it would prevent me from saving the custom tag attribute and value, so I “borrowed/stole” code from it, and then added my modifications.

I then added new Tag Literal and Edit controls to the “Internal Link” dialog, and also updated the CodeBeside xml element to point to my new class (I copy and pasted this from /sitecore/shell/Applications/Dialogs/InsertLink.InsertLink.xml and put my new file into /sitecore/shell/Override/InternalLink/InsertLink.xml in my website root — always put custom Sheer UI dialogs XML files in /sitecore/shell/Override/ so that you don’t run into issues when upgrading Sitecore):

<?xml version="1.0" encoding="utf-8" ?>
<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">
  <InternalLink>
    <FormDialog Icon="Network/32x32/link.png" Header="Internal Link" Text="Select the item that you want to create a link to and specify the appropriate properties." OKButton="OK">
      <Stylesheet Key="Style">
        .ff input { 
          width: 160px;
        }        
      </Stylesheet>
      <CodeBeside Type="Sitecore.Sandbox.Shell.Applications.Dialogs.InternalLink.TagInternalLinkForm, Sitecore.Sandbox" />

      <DataContext ID="InternalLinkDataContext"/>
     
      <GridPanel Columns="2" Width="100%" Height="100%" CellPadding="4" Style="table-layout:fixed">
        <Scrollbox Width="100%" Height="100%" Class="scScrollbox scFixSize" Background="window" Padding="0" Border="1px solid #CFCFCF" GridPanel.VAlign="top" GridPanel.Width="100%" GridPanel.Height="100%">
          <TreeviewEx ID="Treeview" DataContext="InternalLinkDataContext" MultiSelect="False" Width="100%"/>
        </Scrollbox>
      
        <Scrollbox Width="256" Height="100%" Background="transparent" Border="none" GridPanel.VAlign="top" GridPanel.Width="256">
          <GridPanel CellPadding="2" Columns="2">
            <Literal Text="Link Description:" GridPanel.NoWrap="true"/>
            <Edit ID="Text"/>
            
            <Literal Text="Anchor:" GridPanel.NoWrap="true"/>
            <Edit ID="Anchor"/>

            <Label for="Target" GridPanel.NoWrap="true"><Literal Text="Target Window:"/></Label>
            <Combobox ID="Target" Width="100%" Change="OnListboxChanged">
              <ListItem Value="Self" Header="Active browser"/>
              <ListItem Value="New" Header="New browser"/>
              <ListItem Value="Custom" Header="Custom"/>
            </Combobox>
            
            <Panel ID="CustomLabel" Background="transparent" Border="none" GridPanel.NoWrap="true" GridPanel.Align="right"><Label For="CustomTarget"><Literal Text="Custom:" /></Label></Panel>
            <Edit ID="CustomTarget" />

            <Literal Text="Style Class:" GridPanel.NoWrap="true"/>
            <Edit ID="Class"/>
            
            <Literal Text="Alternate Text:" GridPanel.NoWrap="true"/>
            <Edit ID="Title"/>

            <Literal Text="Query String:" GridPanel.NoWrap="true"/>
            <Edit ID="Querystring"/>
          
            <Literal Text="Tag:" GridPanel.NoWrap="true"/>
            <Edit ID="Tag"/>
          </GridPanel>
        </Scrollbox>
      
      </GridPanel>
      
    </FormDialog>            
  </InternalLink>
</control>

Likewise, I repeated the steps for the “External Link” dialog’s code-beside class (I’m not going to go into details here since they are the same as the “Insert Link” dialog class above):

using System;

using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Shell.Applications.Dialogs;
using Sitecore.Shell.Applications.Dialogs.ExternalLink;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.Sheer;
using Sitecore.Xml;
using System.Collections.Specialized;
using System.Xml;

namespace Sitecore.Sandbox.Shell.Applications.Dialogs.ExternalLink
{
    public class TagExternalLinkForm : ExternalLinkForm
    {
        private const string TagAttributeName = "tag";

        protected Edit Tag;

        private NameValueCollection customLinkAttributes;
        protected NameValueCollection CustomLinkAttributes
        {
            get
            {
                if (customLinkAttributes == null)
                {
                    customLinkAttributes = new NameValueCollection();
                    ParseLinkAttributes(GetLink());
                }

                return customLinkAttributes;
            }
        }

        protected override void ParseLink(string link)
        {
            base.ParseLink(link);
            ParseLinkAttributes(link);
        }

        protected virtual void ParseLinkAttributes(string link)
        {
            Assert.ArgumentNotNull(link, "link");
            XmlDocument xmlDocument = XmlUtil.LoadXml(link);
            if (xmlDocument == null)
            {
                return;
            }

            XmlNode node = xmlDocument.SelectSingleNode("/link");
            if (node == null)
            {
                return;
            }

            CustomLinkAttributes[TagAttributeName] = XmlUtil.GetAttribute(TagAttributeName, node);
        }

        protected override void OnLoad(EventArgs e)
        {
            Assert.ArgumentNotNull(e, "e");
            base.OnLoad(e);
            if (Context.ClientPage.IsEvent)
            {
                return;
            }

            LoadControls();
        }

        protected virtual void LoadControls()
        {
            string tagValue = CustomLinkAttributes[TagAttributeName];
            if (!string.IsNullOrWhiteSpace(tagValue))
            {
                Tag.Value = tagValue;
            }
        }
        
        protected override void OnOK(object sender, EventArgs args)
        {
            Assert.ArgumentNotNull(sender, "sender");
            Assert.ArgumentNotNull(args, "args");
            string path = GetPath();
            string attributeFromValue = LinkForm.GetLinkTargetAttributeFromValue(Target.Value, CustomTarget.Value);
            Packet packet = new Packet("link", new string[0]);
            LinkForm.SetAttribute(packet, "text", (Control)Text);
            LinkForm.SetAttribute(packet, "linktype", "external");
            LinkForm.SetAttribute(packet, "url", path);
            LinkForm.SetAttribute(packet, "anchor", string.Empty);
            LinkForm.SetAttribute(packet, "title", (Control)Title);
            LinkForm.SetAttribute(packet, "class", (Control)Class);
            LinkForm.SetAttribute(packet, "target", attributeFromValue);

            TrimEditControl(Tag);
            LinkForm.SetAttribute(packet, TagAttributeName, (Control)Tag);

            SheerResponse.SetDialogValue(packet.OuterXml);
            SheerResponse.CloseWindow();
        }

        private string GetPath()
        {
            string url = this.Url.Value;
            if (url.Length > 0 && url.IndexOf("://", StringComparison.InvariantCulture) < 0 && !url.StartsWith("/", StringComparison.InvariantCulture))
            {
                url = string.Concat("http://", url);
            }
                
            return url;
        }

        protected virtual void TrimEditControl(Edit control)
        {
            if (control == null || string.IsNullOrEmpty(control.Value))
            {
                return;
            }

            control.Value = control.Value.Trim();
        }
    }
}

I also added a Label and Edit control for the Tag as I did for the “Insert Link” dialog above (the “out of the box” External Link dialog xml file lives in /sitecore/shell/Applications/Dialogs/ExternalLink/ExternalLink.xml of the Sitecore website root. When creating custom one be sure to put it in /sitecore/shell/override of your website root):

<?xml version="1.0" encoding="utf-8" ?>
<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">
  <ExternalLink>
    <FormDialog Header="Insert External Link" Text="Enter the URL for the external website that you want to insert a link to and specify any additional properties for the link." OKButton="Insert">

      <CodeBeside Type="Sitecore.Sandbox.Shell.Applications.Dialogs.ExternalLink.TagExternalLinkForm, Sitecore.Sandbox"/>

      <GridPanel Class="scFormTable" CellPadding="2" Columns="2" Width="100%">
        <Label For="Text" GridPanel.NoWrap="true">
          <Literal Text="Link description:"/>
        </Label>
        <Edit ID="Text" Width="100%" GridPanel.Width="100%"/>

        <Label For="Url" GridPanel.NoWrap="true">
          <Literal Text="URL:"/>
        </Label>
        <Border>
          <GridPanel Columns="2" Width="100%">
            <Edit ID="Url" Width="100%" GridPanel.Width="100%" />
            <Button id="Test" Header="Test" Style="margin-left: 10px;" Click="OnTest"/>
          </GridPanel>
        </Border>

        <Label for="Target" GridPanel.NoWrap="true">
          <Literal Text="Target window:"/>
        </Label>
        <Combobox ID="Target" GridPanel.Width="100%" Width="100%" Change="OnListboxChanged">
          <ListItem Value="Self" Header="Active browser"/>
          <ListItem Value="New" Header="New browser"/>
          <ListItem Value="Custom" Header="Custom"/>
        </Combobox>

        <Panel ID="CustomLabel" Disabled="true" Background="transparent" Border="none" GridPanel.NoWrap="true">
          <Label For="CustomTarget">
            <Literal Text="Custom:" />
          </Label>
        </Panel>
        <Edit ID="CustomTarget" Width="100%" Disabled="true"/>

        <Label For="Class" GridPanel.NoWrap="true">
          <Literal Text="Style class:" />
        </Label>
        <Edit ID="Class" Width="100%" />

        <Label for="Title" GridPanel.NoWrap="true">
          <Literal Text="Alternate text:"/>
        </Label>
        <Edit ID="Title" Width="100%" />
        
        <Label for="Tag" GridPanel.NoWrap="true">
          <Literal Text="Tag:"/>
        </Label>
        <Edit ID="Tag" Width="100%" />
      </GridPanel>

    </FormDialog>
  </ExternalLink>
</control>

Since the “out of the box” “External Link” dialog isn’t tall enough for the new Tag Label and Edit controls — I had no quick way of changing this since the height of the dialog is hard-coded in Sitecore.Shell.Applications.ContentEditor.Link in Sitecore.Client — I decided to create a new Content Editor field for the General Link field — this is further down in this post — which grabs the Url of the dialog and dimensions from a custom pipeline I built (the dimensions live in the patch configuration file that is found later on in this post). This custom pipeline uses the following PipelineArgs class:

using Sitecore.Collections;
using Sitecore.Pipelines;

namespace Sitecore.Sandbox.Pipelines.DialogInfo
{
    public class DialogInfoArgs : PipelineArgs
    {
        public string Message { get; set; }

        public string Url { get; set; }

        public SafeDictionary<string, string> Parameters { get; set; }

        public DialogInfoArgs()
        {
            Parameters = new SafeDictionary<string, string>();
        }

        public bool HasInformation()
        {
            return !string.IsNullOrWhiteSpace(Url);
        }
    }
}

Each dialog defined in a pipeline processor of this custom pipeline will specify the dialog’s Url; it’s message — this is how the code ascertains which dialog to load; and any properties of the dialog (e.g. height).

I then built the following class that serves as a processor for this custom pipeline:

using System;
using System.Xml;

using Sitecore.Collections;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.DialogInfo
{
    public class SetDialogInfo
    {
        protected virtual string ParameterNameAttributeName { get; private set; }

        protected virtual string ParameterValueAttributeName { get; private set; }

        protected virtual string Message { get; private set; }

        protected virtual string Url { get; private set; }

        protected virtual SafeDictionary<string, string> Parameters { get; private set; }

        public SetDialogInfo()
        {
            Parameters = new SafeDictionary<string, string>();
        }

        public void Process(DialogInfoArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if(!CanProcess(args))
            {
                return;
            }

            SetDialogInformation(args);
        }

        protected virtual bool CanProcess(DialogInfoArgs args)
        {
            return !string.IsNullOrWhiteSpace(Message)
                && !string.IsNullOrWhiteSpace(Url)
                && args != null
                && !string.IsNullOrWhiteSpace(args.Message)
                && string.Equals(args.Message, Message, StringComparison.CurrentCultureIgnoreCase);
        }
        
        protected virtual void SetDialogInformation(DialogInfoArgs args)
        {
            args.Url = Url;
            args.Parameters = Parameters;
        }

        protected virtual void AddParameter(XmlNode node)
        {
            Assert.ArgumentNotNullOrEmpty(ParameterNameAttributeName, "ParameterNameAttributeName");
            Assert.ArgumentNotNullOrEmpty(ParameterValueAttributeName, "ParameterValueAttributeName");
            if (node == null || !IsAttributeSet(node.Attributes[ParameterNameAttributeName]) || !IsAttributeSet(node.Attributes[ParameterValueAttributeName]))
            {
                return;
            }

            Parameters[node.Attributes[ParameterNameAttributeName].Value] = node.Attributes[ParameterValueAttributeName].Value;
        }

        protected bool IsAttributeSet(XmlAttribute attribute)
        {
            return attribute != null && !string.IsNullOrEmpty(attribute.Value);
        }
    }
}

The Sitecore Configuration Factory injects the dialog’s url, message and parameters into the class instance.

The CanProcess method determines if there is match with the message that is sent via the DialogInfoArgs instance passed to the processor’s Process method. If there is a match, the Url and dialog parameters are set on the DialogInfoArgs instance.

If there isn’t a match, the processor just exits and does nothing.

I then built the following class to serve as a custom Sitecore.Shell.Applications.ContentEditor.Link:

using System;
using System.Collections.Specialized;

using Sitecore.Collections;
using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Shell.Applications.ContentEditor;
using Sitecore.Web.UI.Sheer;

using Sitecore.Sandbox.Pipelines.DialogInfo;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor
{
    public class TagLink : Link
    {
        public override void HandleMessage(Message message)
        {
            Assert.ArgumentNotNull(message, "message");
            if (message["id"] != ID)
            {
                return;
            }

            DialogInfoArgs info = GetDialogInformation(message.Name);
            if (info.HasInformation())
            {
                Insert(info.Url, ToNameValueCollection(info.Parameters));
                return;
            }

            base.HandleMessage(message);
        }

        protected virtual DialogInfoArgs GetDialogInformation(string message)
        {
            Assert.ArgumentNotNullOrEmpty(message, "message");
            DialogInfoArgs args = new DialogInfoArgs { Message = message };
            CorePipeline.Run("dialogInfo", args);
            return args;
        }

        protected virtual NameValueCollection ToNameValueCollection(SafeDictionary<string, string> dictionary)
        {
            if(dictionary == null)
            {
                return new NameValueCollection();
            }

            NameValueCollection collection = new NameValueCollection();
            foreach(string key in dictionary.Keys)
            {
                collection.Add(key, dictionary[key]);
            }

            return collection;
        }
    }
}

The HandleMessage method above passes the message name to the custom <dialogInfo> pipeline and gets back a DialogInfoArgs instance with the dialog’s Url and parameters if there is a match. If there is no match, then the HandleMessage method delegates to its base class’ HandleMessage method (there are dialog Urls and Parameters baked in it).

Now we need to let Sitecore know about the above Content Editor class. We do so like this:

point-to-custom-general-link-in-core

Now that the Content Editor bits are in place, we need some code to render the tag on the front-end of the website. I do this in the following class which serves as a custom <renderField> pipeline processor:

using System.Xml;

using Sitecore.Pipelines.RenderField;
using Sitecore.Xml;

namespace Sitecore.Sandbox.Pipelines.RenderField
{
    public class SetTagAttributeOnLink
    {
        private string TagXmlAttributeName { get; set; }

        private string TagAttributeName { get; set; }
        
        private string BeginningHtml { get; set; }

        public void Process(RenderFieldArgs args)
        {
            if (!CanProcess(args))
            {
                return;
            }

            args.Result.FirstPart = AddTagAttributeValue(args.Result.FirstPart, TagAttributeName, GetXmlAttributeValue(args.FieldValue, TagXmlAttributeName));
        }

        protected virtual bool CanProcess(RenderFieldArgs args)
        {
            return !string.IsNullOrWhiteSpace(TagAttributeName)
                    && !string.IsNullOrWhiteSpace(BeginningHtml)
                    && !string.IsNullOrWhiteSpace(TagXmlAttributeName)
                    && args != null
                    && args.Result != null
                    && HasXmlAttributeValue(args.FieldValue, TagAttributeName)
                    && !string.IsNullOrWhiteSpace(args.Result.FirstPart)
                    && args.Result.FirstPart.ToLower().StartsWith(BeginningHtml.ToLower());
        }

        protected virtual bool HasXmlAttributeValue(string linkXml, string attributeName)
        {
            return !string.IsNullOrWhiteSpace(GetXmlAttributeValue(linkXml, attributeName));
        }

        protected virtual string GetXmlAttributeValue(string linkXml, string attributeName)
        {
            XmlDocument xmlDocument = XmlUtil.LoadXml(linkXml);
            if(xmlDocument == null)
            {
                return string.Empty;
            }

            XmlNode node = xmlDocument.SelectSingleNode("/link");
            if (node == null)
            {
                return string.Empty;
            }

            return XmlUtil.GetAttribute(TagAttributeName, node);
        }

        protected virtual string AddTagAttributeValue(string html, string attributeName, string attributeValue)
        {
            if(string.IsNullOrWhiteSpace(html) || string.IsNullOrWhiteSpace(attributeName) || string.IsNullOrWhiteSpace(attributeValue))
            {
                return html;
            }

            int index = html.LastIndexOf(">");
            if (index < 0)
            {
                return html;
            }

            string firstPart = html.Substring(0, index);
            string attribute = string.Format(" {0}=\"{1}\"", attributeName, attributeValue);
            string lastPart = html.Substring(index);
            return string.Concat(firstPart, attribute, lastPart);
        }
    }
}

The Process method above delegates to the CanProcess method which determines if the generated HTML by the previous <renderField> pipeline processors should be manipulated — the code should only run it the generated HTML is a link and only when there is a tag attribute set on the field.

If the HTML should be manipulated, we basically add the tag attribute with its value it to the generated link HTML — this is done in the AddTagAttributeValue method.

I then wired everything together via the following patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <controlSources>
      <source mode="on" namespace="Sitecore.Sandbox.Shell.Applications.ContentEditor" assembly="Sitecore.Sandbox" prefix="sandbox-content"/>
    </controlSources>
    <fieldTypes>
      <fieldType name="General Link">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
      </fieldType>
      <fieldType name="General Link with Search">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
      </fieldType>
      <fieldType name="link">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
        </fieldType>
    </fieldTypes>
    <pipelines>
      <dialogInfo>
        <processor type="Sitecore.Sandbox.Pipelines.DialogInfo.SetDialogInfo, Sitecore.Sandbox">
          <ParameterNameAttributeName>name</ParameterNameAttributeName>
          <ParameterValueAttributeName>value</ParameterValueAttributeName>
          <Message>contentlink:externallink</Message>
          <Url>/sitecore/shell/Applications/Dialogs/External link.aspx</Url>
          <parameters hint="raw:AddParameter">
            <parameter name="height" value="300" />
          </parameters>
        </processor>
      </dialogInfo>
      <renderField>
        <processor patch:after="processor[@type='Sitecore.Pipelines.RenderField.GetInternalLinkFieldValue, Sitecore.Kernel']" 
                   type="Sitecore.Sandbox.Pipelines.RenderField.SetTagAttributeOnLink, Sitecore.Sandbox">
          <TagXmlAttributeName>tag</TagXmlAttributeName>
          <TagAttributeName>tag</TagAttributeName>
          <BeginningHtml>&lt;a </BeginningHtml>
        </processor>  
      </renderField>
    </pipelines>
  </sitecore>
</configuration>

Let’s try this out!

For testing I added two General Link fields to the Sample Item template (/sitecore/templates/Sample/Sample Item in the master database):

added-two-general-link-fields

I also had to add two Link field controls to the sample rendering.xslt that ships with Sitecore:

added-two-link-field-controls

Let’s test the “Insert Link” dialog:

set-tag-value-insert-link

After clicking the “OK” button and saving the Item, I looked at the “Raw values” on the field and saw that the tag was added to the field’s xml:

tag-value-set-insert-link

Let’s see if this works on the “Insert External Link” dialog:

set-tag-value-insert-external-link

After clicking the “OK” button and saving the Item, I looked at the “Raw values” on the field and saw that the tag was added to the field’s xml:

tag-value-set-insert-external-link

After publishing everything, I navigated to my home page and looked at its rendered HTML. As you can see, the tag attributes were added to the links:

links-front-end-rendered

If you have any comments or thoughts on this, please drop a comment.

Until next time, keep on Sitecoring!