Archive for category MVC

Sitecore RenderingsMarkerModule

Note: This module has been created as a package and is available for download along with the complete source code at Git: aceanindita

Sitecore being component based as it is, adding start and end comments to your views can be immensely helpful while debugging issues, especially if they are being worked on someone who doesn’t know the project from Adam.

Hardcoding the start / end comments isn’t a great idea for obvious reasons like being prone to developer omission in some views, refactoring / file renaming concerns etc.

In an earlier post I had shown how we had partially resolved this by making the build up of the comment dynamic using a html helper method which used the WebPageExecutingBase.VirtualPath property from the System.Web.WebPages assembly.
You can refer the post here: Using dynamic view markers in Sitecore MVC

In this method, you would need to manually wrap each of your views within a call to the said html helper method, which would dynamically include the current view path in start and end comments around your view. While this did away with the potential issues that might come out of refactoring like changing folder structures / renaming files, it still didn’t deal with the dependency on having the developer(s) remember to add the call to this html helper in every single view.

Here’s a solution for this, we added a new processor in the mvc.rendering pipeline, before and after the call of Sitecore.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer which renders the actual rendering html.

Here’s the config update made for this (patch):

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <mvc.renderRendering>
        <processor type="RenderingsMarkerModule.Pipelines.MVC.RenderingMarker.RenderMarkerStart, RenderingsMarkerModule"
            patch:before="processor[@type='Sitecore.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer, Sitecore.Mvc']" />
        <processor type="RenderingsMarkerModule.Pipelines.MVC.RenderingMarker.RenderMarkerEnd, RenderingsMarkerModule"
            patch:after="processor[@type='Sitecore.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer, Sitecore.Mvc']" />
      </mvc.renderRendering>
    </pipelines>
    <settings>
      <setting name="RenderingsMarkerModule.Enabled" value="true" />
      <setting name="RenderingsMarkerModule.ShowLayoutMarkers" value="false" />
    </settings>
  </sitecore>
</configuration>

Note we have 2 additional settings here. ‘RenderingsMarkerModule.Enabled’ lets you toggle the rendering of comments on and off – this could help you turn of this rather revealing feature on staging / production. Additionally you could toggle ‘RenderingsMarkerModule.ShowLayoutMarkers’ to show / hide comments before & after your layout. It is recommended that this remain off, since having your html response start with a html comment might not always be favorable.

Here’s the code we have in place:
RenderingsMarkerModule.Pipelines.MVC.RenderingMarker.RenderMarkerStart

using Sitecore.Configuration;
using Sitecore.Mvc.Pipelines.Response.RenderRendering;
using Sitecore.Mvc.Presentation;

namespace RenderingsMarkerModule.Pipelines.MVC.RenderingMarker
{
    public class RenderMarkerStart : RenderRenderingProcessor
    {
        public override void Process(RenderRenderingArgs args)
        {
            if (Settings.GetBoolSetting("RenderingsMarkerModule.Enabled", false))
            {
                Renderer renderer = args.Rendering.Renderer;
                if (renderer == null)
                    return;

                bool isLayout = renderer is ViewRenderer &&
                                ((ViewRenderer) renderer).Rendering.RenderingType == "Layout";
                bool showLayoutMarkers = Settings.GetBoolSetting("RenderingsMarkerModule.ShowLayoutMarkers", false);

                if (isLayout && !showLayoutMarkers) return;

                args.Writer.Write("\n<!-- START: " + renderer + " -->\n");
            }
        }
    }
}

RenderingsMarkerModule.Pipelines.MVC.RenderingMarker.RenderMarkerEnd

using Sitecore.Configuration;
using Sitecore.Mvc.Pipelines.Response.RenderRendering;
using Sitecore.Mvc.Presentation;

namespace RenderingsMarkerModule.Pipelines.MVC.RenderingMarker
{
    public class RenderMarkerEnd : RenderRenderingProcessor
    {
        public override void Process(RenderRenderingArgs args)
        {
            if (Settings.GetBoolSetting("RenderingsMarkerModule.Enabled", false))
            {
                Renderer renderer = args.Rendering.Renderer;
                if (renderer == null)
                    return;

                bool isLayout = renderer is ViewRenderer &&
                                ((ViewRenderer) renderer).Rendering.RenderingType == "Layout";
                bool showLayoutMarkers = Settings.GetBoolSetting("RenderingsMarkerModule.ShowLayoutMarkers", false);

                if (isLayout && !showLayoutMarkers) return;

                args.Writer.Write("\n<!-- END: " + renderer + " -->\n");
            }
        }
    }
}

Here’s a sample of the output

This module has been created as a package and is available for download along with the complete source code at Git: aceanindita

, , , , , , , , , , , , , , , , , ,

Leave a comment

Multisite configuration for shared Sitecore rendering parameters

On regular templates, we can use sitecore queries to accommodate for showing items in Sitecore link / list fields from respective site nodes in multi site instances.

For Eg:

query:./ancestor::*[@@templatename='Site Node']/Global/Components/Data/Tip Types/*

However, this will not apply to rendering parameter templates. Since while selecting the rendering parameter values in the presentation of an item, the context item is the rendering itself and not the item on which the presentation is being set.
So if we were using a common rendering parameters template for items in multiple sites, we need to rig something up, so that the options appearing in the link / list type rendering parameters show options from the respective site node.

As an example, consider a rendering parameter template, with a background color field.
2015-10-31_235421

This rendering parameter is set to a shared rendering which is used on multiple sites. However, each site has its own unique list of background color options.
To solve this issue, we needed to add support for the token like $sitenode in the example above.

The handler code we write, replaces this token with the right path, after identifying which item the presentation is being set on, and then identifying its sitenode ancestor.

using Sitecore.Data.Items;
using Sitecore.Shell.Applications.ContentEditor;
using Sitecore.Shell.Applications.ContentManager;
using Sitecore.Text;
using Sitecore.Web;
using System.Configuration;
using System.Linq;
using SharedSource.Helpers;

namespace MySite.Customizations.Customized_Sitecore
{
    public class MultisiteRenderingParamDroplink : LookupEx
    {
        public new string Source
        {
            get { return base.Source; }
            set
            {
                if (value.Contains("$sitenode"))
                {
                    Item contextItem = Sitecore.Context.ContentDatabase.Items[ItemID];

                    if (contextItem.Template.BaseTemplates
                        .Any(x => x.ID == ItemTree.Templates.System.Layout.Rendering_Parameters.Standard_Rendering_Parameters.ItemID))
                    {
                        string url = WebUtil.GetQueryString();
                        if (!string.IsNullOrWhiteSpace(url) && url.Contains("hdl"))
                        {
                            FieldEditorParameters parameters = FieldEditorOptions.Parse(new UrlString(url)).Parameters;
                            var currentItemId = parameters["contentitem"];
                            if (!string.IsNullOrEmpty(currentItemId))
                            {
                                Sitecore.Data.ItemUri contentItemUri = new Sitecore.Data.ItemUri(currentItemId);
                                contextItem = Sitecore.Data.Database.GetItem(contentItemUri);

                                Item siteNode = SitecoreHelper.GetAncestorOrSelfByTemplateId
                                    (contextItem, ConfigurationManager.AppSettings["TemplateGuid_SiteNode"]);

                                if (siteNode != null)
                                {
                                    base.Source = value.Replace("$sitenode", siteNode.Paths.FullPath);
                                    return;
                                }
                            }
                        }
                    }
                }
                base.Source = value;
            }
        }
    }
}

Helpers:

            public static Item GetAncestorOrSelfByTemplateId(Item item, string templateId)
            {
                if (item == null || string.IsNullOrEmpty(templateId))
                {
                    return null;
                }

                ID parsedTemplateId;
                return ID.TryParse(templateId, out parsedTemplateId)
                           ? GetAncestorOrSelfByTemplateId(item, parsedTemplateId)
                           : null;
            }

            public static Item GetAncestorOrSelfByTemplateId(Item item, ID templateId)
            {
                if (item == null || templateId == ID.Null || templateId == ID.Undefined)
                {
                    return null;
                }

                Item templateItem = item.Database.GetItem(templateId);
                return templateItem == null ? null : GetAncestorOrSelfByTemplateItem(item, templateItem);
            }

We then register an extension with this type in sitecore:

    <controlSources>
      <source mode="on" namespace="MySite.Customizations.Customized_Sitecore" assembly="MySite" prefix="contentExtension" />
    </controlSources>

This extension is then added to the regular Droplink field. The logic outlined above will kick in only when the current item has the template: /sitecore/templates/System/Layout/Rendering Parameters/Standard Rendering Parameters as its base template. Which would ideally apply to all rendering parameter templates.

2015-11-01_022532

Now the ‘$sitenode’ token would get translated as per the logic added in the above code to the respective site node.

, , , , , , , , , , ,

Leave a comment

Storing user profile image using Salesforce Connector with Sitecore

When using the salesforce connector with Sitecore, we came across the need to store user profile images as well in salesforce.
Once the connector is set up (which includes setup in Web.config / Domains.config and S4S.config), any user user profile interaction automatically happens with salesforce, and fields which are mapped in web.config with corresponding sitecore fields – will automatically get mapped / updated on these interactions!

The special case we came across, was with Profile images. Our site allowed the user to upload profile images, which we needed to store in salesforce, and additionally also wanted the image to show up in sitecore.

The solution we came up with, was to create a document folder in salesforce and upload all profile images there with the contact id as the file name as a document, and then store the url in the Sitecore user portrait field.

In the view, we added the form to upload images:

using (Html.BeginForm("SubmitProfilePhoto", "SharedUpdateProfilePhoto",
    FormMethod.Post, new Dictionary<string, object>
    {
        {"enctype", "multipart/form-data"},
        {"role", "form"}
    }))
{
     <input type="file" accept="image/jpeg,image/gif,image/png" name="profilePhoto" id="profilePhoto" />
}

In the post method:

        [HttpPost]
        public void SubmitProfilePhoto(HttpPostedFileBase profilePhoto)
        {
            BinaryReader binaryReader = new BinaryReader(profilePhoto.InputStream);
            byte[] bytes = binaryReader.ReadBytes((int)profilePhoto.InputStream.Length);

            string documentId = S4SHelper.AddDocument(Convert.ToBase64String(bytes), profilePhoto.FileName.Substring(profilePhoto.FileName.LastIndexOf('.')));

            if (documentId != null)
            {
                // Update Salesforce field in user object - which contains the profile picture document id
                S4SHelper.UpdateContactField(Constants.Salesforce.ContactFields.ContactProfilePhotoDocumentId, documentId);
                // Update profile image url in sitecore user profile                        
                Sitecore.Context.User.Profile.Portrait = S4SHelper.GetDocumentUrl(documentId);
            }

        }

Here’s the code used to add the document in salesforce:

public static string AddDocument(string base64FileContents, string fileExtension)
        {
            if (string.IsNullOrWhiteSpace(_Salesforce_Profile_Image_Folder_Id)) return string.Empty;

            UserInformation userInformation = GetUserInformation();
            DocumentService documentService = new DocumentService(GetSalesforceSession);
            Document document;

            // If a document already exists for this user, delete it! 
            // (We dont want to update it, because image with the same url gets cached)
            if (!string.IsNullOrWhiteSpace(userInformation.ProfilePhotoDocumentId))
            {
                document = documentService.GetByEntityId(userInformation.ProfilePhotoDocumentId);
                if (document != null)
                {
                    documentService.DeleteEntity(document);
                }
            }

            document = new Document
            {
                Name = userInformation.ContactId + fileExtension,
                Body = base64FileContents,
                FolderId = _Salesforce_Profile_Image_Folder_Id,
                IsPublic = true
            };

            SaveResult saveResult = documentService.Save(document);

            if (saveResult.success)
            {
                return saveResult.id;
            }
            return null;
        }

Once the document is added in salesforce, the document url will give you the base document url of your salesforce instance, which you can use and replace the intended document id to get the right url for each new document created. This is what the S4SHelper.GetDocumentUrl(documentId) method does.

, , , , ,

1 Comment

Using dynamic view markers in Sitecore MVC

While working with Sitecore, we add many renderings in the presentation and that makes up each page that is output for the corresponding item.

We are currently working with Sitecore MVC, and we have traditionally added start and end comments in views manually (copy pasting the path of the current view) to mark the start and end of each component on the page. This helped us debug things, made the output html more readable and maintainable, and lets face it, after a month or two, we find it hard to remember which module was called what!

Recently it was pointed out that this might be a potential security threat and we are exposing details of our folder structure in the production environment, which might not be the best idea.

So to ease the process, and remove this potential threat, we created a html helper extension method which would dynamically output the view path start and end comments subject to an appsetting value being toggled to true. This appsetting key would be toggled to false in any environment where we wouldn’t want the comments to be output!

        public static ViewMarker BeginViewMarker(this HtmlHelper htmlHelper, string filename)
        {
            htmlHelper.ViewContext.Writer.Write(RenderFilenameComment("START", filename));
            return new ViewMarker(htmlHelper.ViewContext, filename);
        }

        private static HtmlString RenderFilenameComment(string position, string filename)
        {
            if (ConfigurationManager.AppSettings["ShowViewMarkers"] == "true")
            {
                return new HtmlString(string.Format("<!-- {0}: {1} -->", position.ToUpper(CultureInfo.CurrentCulture), filename));
            }

            return new HtmlString("");
        }

        public class ViewMarker : IDisposable
        {
            private readonly TextWriter _writer;
            private readonly string _filename;
            public ViewMarker(ViewContext viewContext, string filename)
            {
                _writer = viewContext.Writer;
                _filename = filename;
            }

            public void Dispose()
            {
                _writer.Write(RenderFilenameComment("END", _filename));
            }
        }

And in the view, we would just enclose all the view html / razor within a call to this helper method:

@using (Html.BeginViewMarker(VirtualPath))
{
   ...
   ...
   ...
}

The output would look like:

2015-09-19_223640

, , , , , , , , , ,

1 Comment

Glass field validations in Sitecore views

While using Glass.Mapper with Sitecore MVC, we come across the need very often, to check whether a given field – a link or an image / a video is valid or not.

This would be most necessary in views – where we might not want an empty anchor tag, or a a container html tag to show if the contents were empty. The container tag might have certain CSS associated with it, padding / margins – which might break the design, were the contents empty. We might also have js set to run based on the container tag / classes set on them – which we wouldn’t want running if the contents were empty.

We created extension methods to make repeated checks for validity a little easier.

        public static bool IsValid(this Link link)
        {
            if (link == null || string.IsNullOrWhiteSpace(link.Url) || (link.Type != LinkType.Internal && string.IsNullOrWhiteSpace(link.Text)))
                return false;

            return true;
        }

        public static bool IsValid(this Image image)
        {
            if (image == null || string.IsNullOrWhiteSpace(image.Src))
                return false;

            return true;
        }

        public static bool IsValid(this Video_File video)
        {
            return video != null && video.Video_Url != null && !string.IsNullOrWhiteSpace(video.Video_Url.Url);
        }

While Link and Image – referred above are the standard Glass.Mapper.Sc.Fields classes, Video_File indicated above is actually a template in our site which we use for external videos.

2015-07-05_114306

This enables us to do the following to ensure that there is no scope for null reference exceptions:

2015-07-05_151712

, , , , ,

2 Comments

Allowing highlighted text in simple Sitecore text fields with truncate enabled

In our project, we came across a requirement where the content author wanted to manage text with highlights, and we also needed to be able to truncate this – to show as teasers in certain locations – to maintain the integrity of the design.

2015-07-05_155157

We initially thought of Rich Text fields – but then the business need was to ONLY allow highlighted text as special formats – and nothing else. And this was only for certain fields – so we certainly didn’t want to restrict the rich text editor itself either. Additionally allowing full blown html, made the truncation bit a little dicey.

So here’s the solution we came up with.

We picked a custom tag and had the content authors enter the text with those tags. For example, for the above image, the content managed in the sitecore field (single line or multiline – plain text field) was: [highlight]Test[/highlight] caption with [highlight]highlight[/highlight]

While rendering this field on the view:

2015-07-05_155935

In the above example, we not only transform the input text to include highlights, we are also keeping in mind page editor support, and further – we also limit the output text to a certain character count.

public static string GetHighlightedTextHtml(string textToTransform, int characterLimit = 0, bool appendEllipses = false)
        {
            if (string.IsNullOrWhiteSpace(textToTransform))
            {
                return string.Empty;
            }

            string rawText = textToTransform
               .Replace("[highlight]", string.Empty)
               .Replace("[/highlight]", string.Empty);

            characterLimit = TextUtilityStatic.TruncTextToSpecificNumOfChars(rawText, characterLimit).Length;

            string transformedHtml = textToTransform
                .Replace("[highlight]", "<em class=\"highlight--yellow\">")
                .Replace("[/highlight]", "</em>");

            if ((characterLimit > 0 && characterLimit >= rawText.Length) || characterLimit == 0)
            {
                return transformedHtml;
            }

            bool inTag = false;
            int textCharCounter = 0, htmlCharCounter = 0;

            foreach (Char c in transformedHtml)
            {
                if (c == '<')
                {
                    inTag = true;
                    htmlCharCounter++;
                    continue;
                }

                if (c == '>')
                {
                    inTag = false;
                    htmlCharCounter++;
                    continue;
                }

                if (inTag)
                {
                    htmlCharCounter++;
                    continue;
                }

                // Out of tag
                if (characterLimit <= textCharCounter)
                {
                    break;
                }
                htmlCharCounter++;
                textCharCounter++;
            }

            transformedHtml = transformedHtml.Substring(0, htmlCharCounter);

            if (inTag)
            {
                transformedHtml += "</em>";
            }

            return transformedHtml + (appendEllipses ? "..." : "");
        }

Additionally we have logic in place to truncate text to the given character limit – to the nearest word and optionally append ellipses to the text.

        public static string TruncTextToSpecificNumOfChars(string str, int maxCharCount, bool appendEllipses = false)
        {
            if (string.IsNullOrWhiteSpace(str)) return string.Empty;

            if (str.Length <= maxCharCount || maxCharCount == 0)
                return str;

            var originalStrLen = str.Length;

            if (str.Length > maxCharCount)
                str = str.Substring(0, maxCharCount + 1);

            var ellipsePos = str.LastIndexOfAny(new[] { ' ', '.', ',', ';', '!', '-', ']', '}', ')', '*' });
            if (ellipsePos != -1 && str.Length > ellipsePos && ellipsePos > 0)
                str = str.Substring(0, ellipsePos);

            if (ellipsePos == -1)
            {
                str = str.Substring(0, str.Length - 1);
            }

            if ((str.Length < originalStrLen) && appendEllipses)
            {
                str = str.TrimEnd('.').TrimEnd(',').TrimEnd(';').TrimEnd('-');
            }

            return str + ((str.Length < originalStrLen) && appendEllipses ? "..." : string.Empty);
        }

, , ,

Leave a comment

EditFrame with Sitecore MVC and Glass.Mapper

Ever hand to deal with html which is just not compatible with the tags that glass html helpers render for you?
And in these occasions, it pretty much seems like you are going to have to say bye bye to page editor support!
EditFrame is the answer to your problems.

This is definitely not a new concept and existed in the asp.net version of sitecore libraries too.
In this post, I will demonstrate how to get this set up in your MVC site (with glass mapper).

EditFrame basically lets you make any html page editor friendly. All you need to do, is tell it which item and which field / set of fields you want to associate with the given html.

There are 2 parts to setting this up

  • Create items with the fields you want to add support for specified
  • Add the corresponding code – passing in the item which needs to be made editable, wrapped around the html which will be editable

In this example, consider a simple Product template, with a multilist field ‘Categories’

2015-06-07_013912

Core Items

In core database, under /sitecore/content/Applications/WebEdit/Edit Frame Buttons, add your field, making a duplicate of the /sitecore/content/Applications/WebEdit/Edit Frame Buttons/Default folder for convenience. This will bring over with it the below items.

2015-06-07_014505

The title and tooltip in the above screenshot will correspond to the title and tooltip of the edit frame in page editor mode when you select the editable target html.

You can then specify the fields for which you would want to add page editor support. In this example, I have mentioned only 1, but as mentioned below, you can add a pipe delimited set of multiple fields.

2015-06-07_015230

Another great advantage of this approach, of separating the fields from the items / templates here, is that you can create a single folder for common fields like say Background Image, Products, Featured Links etc, which might be reused over multiple templates!

So now in the code, you need to create the frame tag and pass in the path to this folder we created, along with the item which needs to be editable.

Following is the view code:

@using sc72
@using sc72.tdsmaster.sitecore.templates.User_Defined

@inherits Glass.Mapper.Sc.Web.Mvc.GlassView<Product>

<!-- START: Product Detail -->

<b>Sku:</b> @Editable(p => p.Sku)
<b>Name:</b> @Editable(p => p.Name)
@RenderImage(p => p.Image, isEditable: true)
@using (BeginEditFrame(ItemTree.Content.Applications.WebEdit.Edit_Frame_Buttons.Site.Status.ItemPath, Model.Id.ToString()))
{
    if (Model.Custom_Status != null)
    {
        <span><b>Status:</b> @Model.Custom_Status.Title</span>
    }
}

@using (BeginEditFrame(ItemTree.Content.Applications.WebEdit.Edit_Frame_Buttons.Site.Categories.ItemPath, Model.Id.ToString()))
{
    if (Model.Custom_Categories != null && Model.Custom_Categories.Any())
    {
        <span><b>Categories:</b> </span>
        foreach (Product_Category category in Model.Custom_Categories)
        {
            @:&nbsp;&nbsp;&nbsp;&nbsp; @category.Title

        }
    }
}

@RenderLink(p => p.Purchase_Link, isEditable: true)

<!-- END: Product Detail -->

Here’s the resultant page in pageeditor mode:

2015-06-07_015929

Here are examples of scenarios when this is particularly useful:

  • Background images
  • Page editor support for sitecore reference type fields (droplinks, treelists, multilists etc)
  • Iframes / Audio / Video tags
  • Rendering sitecore data to get plugins on your page
  • Newer html tags – picture tag / canvas – Or really any HTML for which glass might not have page editor support included html helper methods.

You can also configure the above to allow inserting new items in page editor mode!

, , , , , ,

2 Comments

Using Custom Routes in Sitecore MVC

Many a time, we come across the need to customize our urls in sitecore.
While doing this would definitely need overriding the item resolver in sitecore, additionally we use MVC routes to be able to define the route structure we need.

In our project, we found this especially useful for bucketable items and also certain special / listing pages – where we used the JS History API to account for paging / sorting filtering and the likes.

Consider the scenario of bucketable product items:

2015-06-01_012328

Now, we would definitely not want the product url correspond with the sitecore structure as shown above. It would be more likely that we would want the product url to perhaps embed a super category name, a category name, and may be a sku along with the product name itself. In this case, we will implement custom routes and also override the sitecore item resolver and link provide to make the circle complete.
This kind of implementation will also let you have separate presentations on every item if you wanted to, as opposed to the approach of using sitecore wildcard nodes where the presentation couldn’t be updated by the content author for individual items.

To achieve the url we want, the route would need to be defined:

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
         routes.MapRoute("Products", "products/{supercategory}/{category}/{productname}/{sku}");
    }
}

Point to note: If you have multiple routes which are similar – in the sense that they start with the same sequence, you will want to add most specific to most generic route to your route table.

And in your global.asax.cs, you need to register the route(s) you just created:

public override void Application_Start()
{
     AreaRegistration.RegisterAllAreas();
     ...
     RouteConfig.RegisterRoutes(RouteTable.Routes);
}

Now you will need to add to the httpRequestBegin sitecore pipeline – a new itemresolver pipeline where you need to add the logic to identify the item from the route you just created above.
Basically, for any product, if you were to get the identifiers that were declared ({supercategory},{category},{productname},{sku} in this case), the code which fetches the corresponding product item from sitecore based on these identifiers, needs to be in the itemresolver code.

In this case, we will assume that the sku is a unique identifier for a product.
Note: If you are using an item name as an identifier, you will want to ensure that the item name is unique for the template you are searching for (in the location you are searching, if it is not the entire tree). It would be best to ensure that the item names are unique in this case, in the scope you will define for the item search. You could use this post, to ensure this and add a constraint to this effect: Unique item name constraint using index search in Sitecore.

namespace myassemblyname
{
    public class CustomItemResolver : HttpRequestProcessor
    {
        public override void Process(HttpRequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (Sitecore.Context.Item != null || Sitecore.Context.Database == null || args.Url.ItemPath.Length == 0) return;

            RouteBase route = RouteTable.Routes["Products"];

            if (route == null) return;

            RouteData routeData = route.GetRouteData(new HttpContextWrapper(HttpContext.Current));

            if (routeData == null) return;

            string productSku = (string)routeData.Values["sku"];

            if (string.IsNullOrWhiteSpace(productSku)) return;

            // Logic to fetch the product from sitecore / index based on the sku fetched from the url
            // you could optionally also use the other identifiers like the category / super category etc.
            Item product = ...

            if (product == null) return;

            Sitecore.Context.Item = product;
        }
    }
}

The config update here to add the above processor:

<pipelines>
    <httpRequestBegin>
        <processor type="myassemblyname.CustomItemResolver, myassemblyname" />
    </httpRequestBegin>
</pipelines>

In addition to this, to complete the circle here, you will also want to customize the your sitecore link provider here. This is so that the product urls when requested from the LinkManager are returned in the format you want it to, instead of the one corresponding to the sitecore folder structure (buckets in this case)

namespace myassemblyname
{
    public class CustomLinkProvider : LinkProvider
    {
        public override string GetItemUrl(Item item, UrlOptions urlOptions)
        {
            var holdUrlOptions = urlOptions;
            try
            {
                urlOptions.SiteResolving = Settings.Rendering.SiteResolving;

                string customUrl;

                // We want this to apply to only Products
                if (item.TemplateID.ToString() == "<Product template guid>")
                {
                    // Code to fetch the sku / category / super category of the product given that we have the product item itself in 'item'
                    string category = ...
                    string superCategory = ...

                    if (!string.IsNullOrWhiteSpace(category) && !string.IsNullOrWhiteSpace(superCategory))
                    {
                        // NOTE, if this is being called from an asynchronous tasks, context will be null
                        // in this case, we will need to set the url by replacing the identifiers with the corresponding values in the routes
                        if (HttpContext.Current != null)
                        {
                            var urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext);
                            customUrl = urlHelper.RouteUrl("Products", new
                            {
                                supercategory,
                                category,
                                item.Name,
                                sku
                            });
                        }
                    }
                }
                catch
                {
                    urlOptions.SiteResolving = Settings.Rendering.SiteResolving;
                    return base.GetItemUrl(item, holdUrlOptions);
                }
            }
        }
    }
}

The linkprovider override will also need to be applied in the config:

<linkManager defaultProvider="custom">
    <providers>
        <add name="custom" type="myassemblyname.CustomLinkProvider, myassemblyname" addAspxExtension="false" alwaysIncludeServerUrl="false" encodeNames="true" languageEmbedding="never" languageLocation="filePath" lowercaseUrls="true" shortenUrls="true" useDisplayName="false" />
    </providers>
</linkManager>

So this should allow you to use product urls like the one below throughout your site.
Note: you are defining the custom route in a single place (in the RegisterRoutes method above), so this is a clean approach as well!

yourdomain.com/products/stationery/printer-cartridges/canon-cl-241/cl241

In addition to the above, you might also come across random scenarios where implementing custom routes may be a great idea. If you decide to use the history api to manage interactions like sorting / paging etc, to support maintaining listing states on page refresh, you will need custom routes implemented.

An example of this scenario, would be a product listing, accessed using yourdomain.com/products.
Say you were on page 2, had sorted by descending order of product name.
And the sameple resulting url would be:

yourdomain.com/products/params/2/ztoa

If you didnt have custom routes implemented, and refreshed the page, you would ideally land on a 404, since the route till ‘products’ corresponds to the sitecore tree, but the remaining don’t.

To get around this, you could use a route definition like:

     routes.MapRoute("ProductListing", "products/params/{*args}");

Having a separator string like ‘params’ would be advised here so that the routes don’t interfere with each other.
Also, {*args} will correspond to a variable number of arguments.
You could also fetch the entire string of these variable arguments in your controller action using the method signature:


public ActionResult CustomIndex(string args = null)
{
     // args will contain the string after params, in this example '2/ztoa'
     // this string could be split and used to return the page to its intended state if required!
}

Happy routing!

, , , ,

Leave a comment

SUG Bangalore Kick Off!

We had our kick off session of the Sitecore User Group Bangalore yesterday (28 March 2015) and a big thank you to everyone who helped make it a success!

The session started with a brief introduction by David Brown, Engagement Manager at Verndale, followed by an Introductory Session to Sitecore 7.2 with MVC, using TDS and Glassmapper by me.

We had 12 attendees at the session with a couple of attendees joining in remotely.

Following is the recording of the session, and the presentation used.

Unfortunately, we went over the anticipated 2 hours and yet fell short of time to be able to cover glassmapper in detail. So we will organize a glassmapper specific session in an upcoming event!

Looking forward to the next session! Date to be announced soon!

Next SUG

Meanwhile – please do join our group and participate in this poll, to determine the timing of the next session!

Few of pictures from this meetup!

DSCN2934 DSCN2938

15 min break! :)

15 min break! 🙂

, , , , , , , , , , , , , , , , ,

Leave a comment