Archive for July, 2015

Custom Multisite Multilist with Search Sitecore field

We are currently working on a multisite sitecore 7.2 with MVC solution. During this time, we came across the need to allow dynamic datasources to Multilist with search sitecore fields.
While looking out for a solution, I came across this post which give a great way of inducing queries into the datasource of a multilist with search field: Dynamic Multilist with Search.

However, we also had instances in our site, where the repository lived outside of the site node itself. So we needed to build something custom here.

For example: Sitecore Commerce Connect’s product repository lives outside of the home node:
2015-07-06_005755
Similarly, we also had video repositories for each site which live inside the Media Library.

So we ended up creating a custom field to cater to our needs here. This new field, took as input in the search location parameter – certain special tokens, which we then resolved in the code. In addition, we also used filters to filter through multiple templates – since each site might have separate template for say products etc.

In our solution in sitecore, we created a common template – Site Node – which was used to create each site’s root, and the respective Home nodes were child items of these Site Nodes.
Any external repositories which were associated with respective sites, were associated using fields on the site node.

Example, for Site 1:
2015-07-06_011233

In line with the requirements we had, we created support for 3 special tokens

  • $sitenode
  • $siteproductsrepository
  • $sitevideorepository

So basically in the datasource of this new field, we would use values like:

*************************** PRODUCTS – Multisite Multilist with Search *****************************
StartSearchLocation=$siteproductrepository&Filter=_templatename:Site1 Product|_templatename:Site2 Product

************************* Product Category – Multisite Multilist with Search ***********************
StartSearchLocation=$sitenode/Home/Products&Filter=_templatename:Site1 Product Category|_templatename:Site2 Product Category

************************* Video – Multisite Multilist with Search ***********************
StartSearchLocation=$sitevideorepository&Filter=_templatename:Video File

The code used:

namespace MySite._Classes.SBDShared.Customizations.Customized_Sitecore
{
    class MultisiteMultilistWithSearchField : BucketList
    {
        public new string Source
        {
            get { return base.Source; }
            set
            {
                if (value.Contains("$siteproductrepository"))
                {
                    var contextItem = Sitecore.Context.ContentDatabase.Items[ItemID];
                    var siteNode = SitecoreHelper.ItemMethods.GetAncestorOrSelfByTemplateId(contextItem, ConfigurationManager.AppSettings["TemplateGuid_SiteNode"]);
                    if (siteNode != null)
                    {
                        var productRepository = SitecoreHelper.ItemRenderMethods.GetReferenceField(siteNode, ISite_NodeConstants.Product_RepositoryFieldName);
                        base.Source = productRepository != null ? value.Replace("$siteproductrepository", productRepository.ID.ToString()) : value;
                    }
                }
                else if (value.Contains("$sitevideorepository"))
                {
                    var contextItem = Sitecore.Context.ContentDatabase.Items[ItemID];
                    var siteNode = SitecoreHelper.ItemMethods.GetAncestorOrSelfByTemplateId(contextItem, ConfigurationManager.AppSettings["TemplateGuid_SiteNode"]);
                    if (siteNode != null)
                    {
                        var videoRepository = SitecoreHelper.ItemRenderMethods.GetReferenceField(siteNode, ISite_NodeConstants.Video_RepositoryFieldName);
                        base.Source = videoRepository != null ? value.Replace("$sitevideorepository", videoRepository.ID.ToString()) : value;
                    }
                }
                else if (value.Contains("$sitenode"))
                {
                    const string startSearchLocationText = "StartSearchLocation=";
                    var sourceString = value;
                    var contextItem = Sitecore.Context.ContentDatabase.Items[ItemID];

                    int startSearchLocationIndex = sourceString.IndexOf(startSearchLocationText, System.StringComparison.OrdinalIgnoreCase);

                    string startSearchLocation = sourceString.Substring(startSearchLocationIndex + startSearchLocationText.Length,
                        sourceString.IndexOf("&", startSearchLocationIndex, System.StringComparison.Ordinal) - startSearchLocationText.Length);

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

                    if (siteNode != null)
                    {
                        var sourceItem = SitecoreHelper.ItemMethods.GetItemByPathInMaster(startSearchLocation.Replace("$sitenode", siteNode.Paths.FullPath));
                        base.Source = sourceItem != null ? value.Replace(startSearchLocation, sourceItem.ID.ToString()) : value;
                    }
                }
                else
                {
                    base.Source = value;
                }
            }
        }
    }
}

2 special custom sitecore helper methods are used here:

  • GetAncestorOrSelfByTemplateId: Basically goes recursively up the content tree to find an ancestor item match by template id.
  • GetReferenceField: This method gets the item referenced in say a droplink field, by fetching the item based on the guid stored in the field.

Finally, to make this new control available in sitecore – we need to add a corresponding item in the core database (Make a copy from the similar Multilist with Search for simplicity here).
2015-07-06_014516

A corresponding config update would be required to register the prefix used above:

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

, , , , , , , ,

1 Comment

Export Item Data Tool (Module) for Sitecore

Download Here

Often we come across situations where we need to export data from our Sitecore instance into common formats like excel / xml / json etc. Usually we end up writing ad hoc scripts for this based on the requirements.

This is exactly what this tool does. It allows you to write sort of a query to determine what data you’d want, and let you export this data to the common formats like Excel / XML and Json.

This tool lets you nest fields to account for reference fields too. As an example, lets look at this scenario – I have a bunch of Article items, and I need to export this data into excel.

This was the structure in place:

2015-07-05_210728

So for the above templates, we would use the following query: Headline,Article Type(Title),Necessary Products(Sku,ProductType(Title)),Skill Level(Title)

The module basically does an index search and hence also needs the comma separated template id(s), search location / language and index name to do the search. In addition to the custom fields that can be added, we could also choose to include common attributes like Item Id, Item Name etc.

Here’s another simple example with the sample output data:

2015-07-05_212326

2015-05-31_114306

A look at sitecore:

2015-07-05_212710

Sample Output:

JSON

2015-07-05_213406

XML

2015-07-05_213512

The code for this tool is available in github: https://github.com/aceanindita/sitecore-export-item-data-tool

Once installed, the tool appears in the sitecore menu as:

2015-07-05_213810

To see the steps to create a sitecore module please refer: Creating a Sitecore Module

, , , , , , , , , ,

4 Comments

Creating a Sitecore Module

Following are the steps to create a Sitecore Module. This work is on Sitecore 7.2. Most of this information is already available, but while creating a module, I faced a few issues, to fix which, I had to look around a bit for a solution. This is why I am providing the steps I followed. Note the raw values shown below – and ensure your field values look similar for things to work right.

Core Database Items:

/sitecore/content/Applications/Export Item Data Tool

2015-07-05_214543

Raw View:

2015-07-05_214604

For the short cut to show up:

/sitecore/content/Documents and settings/All users/Start menu/Right/Export Item Data Tool

2015-07-05_214735

Raw View:

2015-07-05_214700

Here’s the code organization:

2015-07-05_214924

So deliverables in this case, would include a dll and an aspx file in the $\sitecore modules\Web\… path.

The end result:

2015-07-05_213810

2015-07-05_215914

, , ,

1 Comment

Override GlassView.RenderLink/BeginRenderLink for internal items in Sitecore

We are using sitecore 7.2 with TDS and glassmapper, and we came across a need where we needed special logic to render links to specifically internal items.

In our company we follow a certain pattern with items, something I am sure everyone follows themselves. And as a part of the standard set of base fields that every navigable item in sitecore has, we have a Navigation Title and a Redirect Link.

  • A Navigation Title serves the purpose of letting the link text to an item rendered anywhere on the site to be managed separately from the item name / headline itself. You might want a shorter title to links to a page, while the headline of the page itself might be more descriptive. So when we use GlassView.RenderLink – we would like the provision to fallback to this Navigation Title field if no description is manually managed by the content author.
  • A Redirect Link serves the purpose of allowing the content author to temporarily or permanently having an item redirect to a different page (internal or external). In case of external links, this method basically allows us to have placeholder Sitecore items for external links too! Which is especially useful when we want them to show up in Content Tree Navigation renderings. The Redirect Link is at the end of the day a link field. If the content author wanted the external link set in this field to open in a new tab (considering its an external link!), GlassView.RenderLink wouldn’t know to make this happen since this is custom logic we wanted in place based on our custom field. So we needed to make an update to GlassView.RenderLink – to be able to honor the redirect targets as well.

We wrote overrides for RenderLink and also BeginRenderLink methods to cover our bases.

GlassView.RenderLink

        public HtmlString RenderLink<T>(T model, Expression<Func<T, object>> field, object attributes = null,
            bool isEditable = false, string contents = null, bool isContentEmpty = false)
        {
            if (field == null)
                return new HtmlString(string.Empty);

            Link tempLink = field.Compile().Invoke(model) as Link;

            if (tempLink.IsRedirectNewTab())
            {
                if (attributes == null)
                {
                    attributes = new NameValueCollection { { "target", "_blank" } };
                }
                else if (attributes is NameValueCollection)
                {
                    NameValueCollection currentAttributes = attributes as NameValueCollection;

                    if (currentAttributes["target"] != null)
                    {
                        currentAttributes.Remove("target");
                    }
                    currentAttributes.Add(new NameValueCollection { { "target", "_blank" } });
                    attributes = currentAttributes;
                }
            }

            return new HtmlString(GlassHtml.RenderLink(model, field, attributes, isEditable,
                GlobalHelper.GetLinkContents(field.Compile().Invoke(model) as Link, contents, isContentEmpty)));
        }

        public HtmlString RenderLink(Expression<Func<TModel, object>> field, object attributes = null,
            bool isEditable = false, string contents = null, bool isContentEmpty = false)
        {
            return RenderLink(Model, field, attributes, isEditable, contents, isContentEmpty);
        }

GlassView.BeginRenderLink

        public override RenderingResult BeginRenderLink(Expression<Func<TModel, object>> field,
            object attributes = null, bool isEditable = false)
        {
            return BeginRenderLink(Model, field, attributes, isEditable);
        }

        public override RenderingResult BeginRenderLink<T>(T model, Expression<Func<T, object>> field,
            object attributes = null, bool isEditable = false)
        {
            Link tempLink = field.Compile().Invoke(model) as Link;

            if (tempLink.IsRedirectNewTab())
            {
                if (attributes == null)
                {
                    attributes = new NameValueCollection { { "target", "_blank" } };
                }
                else if (attributes is NameValueCollection)
                {
                    NameValueCollection currentAttributes = attributes as NameValueCollection;

                    if (currentAttributes["target"] != null)
                    {
                        currentAttributes.Remove("target");
                    }

                    currentAttributes.Add(new NameValueCollection { { "target", "_blank" } });
                    attributes = currentAttributes;
                }
            }
            return base.BeginRenderLink(model, field, attributes, isEditable);
        }

In BeginRenderLink – we usually are not too concerned about the text – so we haven’t overridden that here.
We have used 2 helper methods here:

  • IsRedirectNewTab() – an extension method to glass Link class to determine if the target of the link is an internal item AND has a redirect link set on it AND the redirect link has a target set to open in a new tab. We could ofcourse go a step further to just pull the target from the link, but the business logic here, called for specifying new window only.
  • GlobalHelper.GetLinkContents() – would return the Navigation Title field – IF no link description was managed AND the link pointed to an internal item.
        public static bool IsRedirectNewTab(this Link link)
        {
            if (link == null || !link.IsValid() || link.Type != LinkType.Internal || link.TargetId == new Guid()) return false;

            Base_Navigation baseNavigation = SharedController.SitecoreCurrentContext.GetItem<Base_Navigation>(link.TargetId);

            if (baseNavigation != null && baseNavigation.Redirect_Link.IsValid())
            {
                return baseNavigation.Redirect_Link.Target.ToLower() == "_blank";
            }

            return false;
        }

Note: We use the singleton pattern to get a single instance of the Glass.Mapper.Sc.SitecoreContext class, which is in SharedController.SitecoreCurrentContext.
Also, we have extension methods for certain classes to validate the corresponding objects (IsValid()). Please find a short note here: Glass field validations in Sitecore views

        public static string GetLinkContents(Link link, string contents, bool isContentEmpty = false)
        {
            if (string.IsNullOrEmpty(contents) && link != null && string.IsNullOrWhiteSpace(link.Text) && !isContentEmpty)
            {
                if (link.Type == LinkType.Internal)
                {
                    Base_Navigation targetItem = SharedController.SitecoreCurrentContext.GetItem<Base_Navigation>(link.TargetId);
                    if (targetItem != null)
                        return targetItem.Navigation_Title;
                }
            }
            return contents;
        }

So now RenderLink will do all the additional processing that we needed without having to make any change to the calls we make in the views to the RenderLink method.
Main thing to note here though, all our views inherit from the custom class we created (instead of GlassView) where we placed these overrides.

namespace MySite.Customizations.Customized_Glass
{
    public abstract class CustomGlassView<TModel> : GlassView<TModel>
    {
        public HtmlString RenderLink<T>(T model, Expression<Func<T, object>> field, object attributes = null,
            bool isEditable = false, string contents = null, bool isContentEmpty = false)
        {
            ...
        }

        public HtmlString RenderLink(Expression<Func<TModel, object>> field, object attributes = null,
            bool isEditable = false, string contents = null, bool isContentEmpty = false)
        {
            ...
        }

        public override RenderingResult BeginRenderLink(Expression<Func<TModel, object>> field,
            object attributes = null, bool isEditable = false)
        {
            ...
        }

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

So our views all inherit CustomGlassView:

@inherits MySite.Customizations.Customized_Glass.CustomGlassView<...>

, , , , , ,

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