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><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):
After doing a build and navigating to my homepage Item, I saw the following in the rendered HTML:
As you can see, it worked magically. 🙂
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. 😀
[…] One Approach to Render a Custom General Link Field Attribute in a Sitecore MVC View Rendering via&nb… […]
[…] One Approach to Render a Custom General Link Field Attribute in a Sitecore MVC View Rendering via&nb… […]
[…] One Approach to Render a Custom General Link Field Attribute in a Sitecore MVC View Rendering via Gl… […]