Archive for September, 2015

Output Html / Encoded characters using glass html helpers in Sitecore

When a content author enters decoded symbols in single line / multiline sitecore text fields, the symbols don’t get encoded on their own on the site when we output them with @Editable (Glass method – supporting page editor mode).
This also applies to links rendered using @RenderLink (Glass method)
This would mean text like ® and ™ getting output in place of ® and ™ respectively.

Sitecore text fields DO support these symbols being pasted on to them directly, but sometimes it is not a choice we can make, especially if we have items created and prefilled automatically through external code / a connector to a third party system etc.

To achieve this, we create a new class inheriting from the GlassView class, and create Editable / RenderLink methods which use the respective base methods, taking care of the HtmlDecode!

public abstract class MyGlassView<TModel> : GlassView<TModel>
    {
        // *********************************** Text ***********************************

        public new HtmlString Editable(Expression<Func<TModel, object>> field, object parameters = null)
        {
            return new HtmlString(HttpUtility.HtmlDecode(GlassHtml.Editable(Model, field, parameters)));
        }

        public new HtmlString Editable(Expression<Func<TModel, object>> field, Expression<Func<TModel, string>> standardOutput, object parameters = null)
        {
            return new HtmlString(HttpUtility.HtmlDecode(GlassHtml.Editable(Model, field, standardOutput, parameters)));
        }

        // *********************************** Links ***********************************

        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);

            return new HtmlString(HttpUtility.HtmlDecode(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);
        }
    }

All views will need to inherit from this new MyGlassView class to be able to use these new methods.

, , , , , , , , ,

Leave a comment

Faceting on product specifications with SOLR computed fields in Sitecore

In most product repositories, we come across product specification sets for each product, which usually comprise of a key value pair.
2015-09-20_015800
We had one such scenario in our project, and we came across the requirement to facet on various specification keys.

We have currently added a list of comma separated (key, value) pairs in the specifications computed field. The computed field needed to be declared as a string collection:

<field fieldName="productspecificationstext" returnType="stringCollection" storageType="YES" indexType="string">
    mysite.ComputedFields.ProductSpecificationsText,mysite
</field>

And in the code for the computed field (implementing IComputedIndexField), you will want to split this list and return it as a List.
So this

public object ComputeFieldValue(IIndexable indexable)

would return List<string>

Here’s a sample from the index:

2015-09-20_020439

So then we facet on this field, and filter out the facets that start with (specification key + ‘,’) for the specification key we want the facets for and remove the preceding key and ‘,’.
So if I were to facet on size specification for the below data (provided we had a separator between the specification key and value), we would get:

2015-09-20_021329

Large (1)
Small (3)
Medium (1)

This way, we use the same computed field to facet on any kind of specification.

The method used would be the Sitecore.ContentSearch method:

2015-09-20_021622

, , , , , , , ,

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

Customize Glass.Mapper T4 Templates for Sitecore TDS CodeGen

If you are using glass.mapper with TDS and say Sitecore MVC, and have in the past created your models manually, you know that glass codegen is a godsend!

Glass basically provides us with t4 template files and dlls, based on the items included in the TDS project, the code is generated using the t4 templates.

We have used TDS with glass for 3-4 projects now and there were a few customizations that we have had to do in the t4 template files to tailor the code gen files to our needs.

Know your t4 template files:
While enabling glass.mapper to auto generate code for your tds items (models and item properties), you will provide 2 templates

  • Transform File (glassv3header.tt) – for the common header and will appear only once in your generated code file – this includes the namespace using statements / base interface / class for all models.
  • Base Project Transform File (glassv3item.tt) – The other template file will be used in a repetitive manner for every item that code (class / model) is generated for.

2015-09-19_172200

In addition to this, we also use the Code Generation Template (itempaths.tt) file to generate item properties for any items we would want. The properties generated per item, is again decided by the template file itself, but out of the box, contains properties such as the item id, item path, template name and template id.

2015-09-19_172313

Add TemplateId to all codegen classes

One thing we noticed right off the bat, was the lack of a property for the template id in the models! We needed this for many situations, like deciding a separate html rendering based on the template id of the item etc. So we updated the t4 template (glassv3header.tt) to include the template id for every model, including the appropriate sitecore attributes on them to get them to populate as expected. (line below)

Item Name vs a Name field

The header template for the common code for your code gen file, has a property mapped to the item name ‘Name’. We came across a situation where we were working with Sitecore Commerce Connect templates, and the main product template there had a Name field. When we tried generating code for that template, we ran into conflicts! We couldn’t change the name of the field in the Commerce Connect template, so had to update the t4 template (glassv3header.tt) instead to change the ‘Name’ property to ‘ItemName’ (line below)

Error with codegen for System Language template

When we tried adding the sitecore system template ‘/sitecore/templates/System/Language’ into TDS and generating code for it, but we again ran into conflicts here! This is why we again updated the t4 template (glassv3header.tt) to explicitly specify the namespace of the Language property. (line below)

<#@ template language="C#"  #>
<#@ assembly name="System.Core" #>

<#@ include file="Helpers.tt" #>
<#@ include file="StringExtensions.tt" #>
<#@ include file="GeneralExtensions.tt" #>
<#@ include file="Inflector.tt" #>

<#@ import namespace="System" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="HedgehogDevelopment.SitecoreProject.VSIP.CodeGeneration.Models" #>

<#@ parameter name="Model" type="HedgehogDevelopment.SitecoreProject.VSIP.CodeGeneration.Models.ProjectHeader" #>
<#@ parameter name="DefaultNamespace" type="System.String" #>

#pragma warning disable 1591
#pragma warning disable 0108
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by Team Development for Sitecore.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

using System;   
using System.Collections.Generic;   
using System.Linq;
using System.Text;
using Glass.Mapper.Sc.Configuration.Attributes;
using Glass.Mapper.Sc.Configuration;
using Glass.Mapper.Sc.Fields;
using Sitecore.Globalization;
using Sitecore.Data;
using SystemSpecialized = System.Collections.Specialized;


<#
// Calculate the top leve namespace as configured in the target project and
// base namespace as defined in the TDS project
string fullNamespace = JoinNamespaces(DefaultNamespace, Model.BaseNamespace);
#>

namespace <#=fullNamespace #>
{

	public partial interface IGlassBase{
		
		[SitecoreId]
		Guid Id{ get; }

		[SitecoreInfo(SitecoreInfoType.Name)]
		string ItemName { get; }

		[SitecoreInfo(SitecoreInfoType.Language)]
		Sitecore.Globalization.Language Language{ get; }

        [SitecoreInfo(SitecoreInfoType.Version)]
        int Version { get; }

		[SitecoreInfo(SitecoreInfoType.Url)]
		string Url { get; }
	}

	public abstract partial class GlassBase : IGlassBase{
		
		[SitecoreId]
		public virtual Guid Id{ get; private set;}

		[SitecoreInfo(SitecoreInfoType.Name)]
        public string ItemName { get; private set; }

		[SitecoreInfo(SitecoreInfoType.Language)]
		public virtual Sitecore.Globalization.Language Language{ get; private set; }

        [SitecoreInfo(SitecoreInfoType.Version)]
        public virtual int Version { get; private set; }

		[SitecoreInfo(SitecoreInfoType.Url)]
        public virtual string Url { get; private set; }
	}
}

Add a new sitecore field type to enable codegen create the right custom field property types

For various reasons, we have had to add custom fields in Sitecore. For these new fields, by default glassmapper will general Object properties unless we provide a hint to the type of data this new custom field will contain.
For example, the Single Select with SearchSingle Select Tree fields are link fields and contain a single guid, while Multisite Multilist with Search is a list field and contains a list of guids. So we need to map the intended property type in the GetGlassFieldType() method of glassv3item.tt template file:

public static string GetGlassFieldType(SitecoreField field)
{
	if (field != null && field.Type != null)
    {
		// Pull out any 'type' param from the custom data field on the field in TDS
		string customType = GetCustomProperty(field.Data, "type");
		string generic = GetCustomProperty(field.Data, "generic");
		
		if (customType != "")
		{
			if (generic != "")
			{
				return string.Format("{0}<{1}>", customType, generic);
			}
			else
			{
				return customType;
			}
		}

		switch(field.Type.ToLower())
		{
			case "tristate":
				return "TriState";
			case "checkbox":
				return "bool";

			case "date":
			case "datetime":
				return "DateTime";

			case "number":
				return "float";

			case "integer":
				return "int";

			case "treelist with search":
			case "treelist":
			case "treelistex":
			case "treelist descriptive":
			case "checklist":
			case "multilist with search":
			case "multisite multilist with search":
			case "multilist":
	            return string.Format("IEnumerable<{0}>", string.IsNullOrEmpty(generic) ? "Guid" : generic);

			case "grouped droplink":
			case "droplink":
			case "lookup":
			case "droptree":
			case "reference":
			case "tree":
			case "single select with search":
			case "multisite single select with search":
				return "Guid";

			case "file":
				return "File";

			case "image":
				return "Image";

			case "general link":
			case "general link with search":
				return "Link";
			
			case "password":
			case "icon":
			case "rich text":
			case "html":
			case "single-line text":
			case "multi-line text":
			case "frame":
			case "text":
			case "memo":
			case "droplist":
			case "grouped droplist":
			case "valuelookup":
				return "string";
			case "attachment":
			case "word document":
				return "System.IO.Stream";	   
			case "name lookup value list":
			case "name value list":
				return "SystemSpecialized.NameValueCollection";                                                                                                                         
			default:
				return "object /* UNKNOWN */";
		}
	}
	else 
	{
	   throw new Exception("There is no 'Type' field on the " + field.Name + " field.");
	}
}

, , , , , ,

Leave a comment

Linq DistinctBy

        public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> items, Func<T, TKey> property)
        {
            return items.GroupBy(property).Select(x => x.First());
        }

, , , ,

Leave a comment

Sanitizing text with regular expressions

Strip out HTML Tags

        public static string StripHtml(string str)
        {
            return Regex.Replace(str, "<.*?>", string.Empty);
        }

Convert string to Alphanumeric text

        public static string ToAlphanumeric(this string param)
        {
            if (string.IsNullOrWhiteSpace(param)) return string.Empty;
            return (new Regex("[^a-zA-Z0-9]")).Replace(param, "");
        }

If you want to allow spaces, modify the regex to “[^a-zA-Z0-9 ]”

, , ,

Leave a comment

Sitecore Rich Text Field (RTF) length validator

In some cases, to maintain the integrity of the design, we might want to restrict the amount of text that the content author can add in the rich text field. For this purpose we created a custom validator and applied the same to rich text fields only.

For single line / multiline fields, this can be easily achieved by simply using regex patterns for validation, with rich text fields, the presence on html, requires more custom logic.

Please note – you can also disallow certain tags in the custom validation code here, like images / canvas / picture etc, if the design calls for it.

We first added a field for the maximum length, to the Template Field item (/sitecore/templates/System/Templates/Template field). which would make this field available in all template fields.

2015-09-18_014147

Following is the code used for the custom validation, which uses this field:

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Data.Validators;
using System;
using System.Globalization;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;

namespace sc72.Customizations.Sitecore
{
    [Serializable]
    public class RichTextLengthValidator : StandardValidator
    {
        public RichTextLengthValidator(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {

        }

        public RichTextLengthValidator()
        {

        }

        public override string Name
        {
            get { return "RichTextLengthValidator"; }
        }

        protected override ValidatorResult Evaluate()
        {
            ItemUri itemUri = ItemUri;

            if (itemUri == null)
                return ValidatorResult.Valid;

            Field field = GetField();
            if (field == null)
                return ValidatorResult.Valid;

            string str1 = field.Value;
            if (string.IsNullOrEmpty(str1))
                return ValidatorResult.Valid;

            Database database = Factory.GetDatabase(itemUri.DatabaseName);
            Item fieldItem = database.GetItem(field.ID, itemUri.Language);
            int maxLength;

            if (!(fieldItem.Fields["Maximum Length"] != null && !string.IsNullOrWhiteSpace(fieldItem.Fields["Maximum Length"].Value)
                && int.TryParse(fieldItem.Fields["Maximum Length"].Value, out maxLength) && maxLength > 0))
                return ValidatorResult.Valid;

            if (StripHtml(str1).Length > maxLength)
            {
                Text = GetText("Character count '{0}' exceeded the limit '{1}' for field \"{2}\".", new[]
                {
                    StripHtml(str1).Length.ToString(CultureInfo.InvariantCulture),
                    maxLength.ToString(CultureInfo.InvariantCulture),
                    field.DisplayName
                });
                return GetFailedResult(ValidatorResult.FatalError);
            }

            return ValidatorResult.Valid;
        }

        protected override ValidatorResult GetMaxValidatorResult()
        {
            return GetFailedResult(ValidatorResult.FatalError);
        }

        public static string StripHtml(string str)
        {
            return Regex.Replace(str, "<.*?>", string.Empty);
        }
    }
}

And we then created a new validation rule item in Sitecore and pointed it to this method in our assembly:

2015-09-18_014346

This rule then needs to be added to the rich text field item, depending upon what actions you would like to trigger the validations at, the rule will need to be added accordingly. Eg. Quick Action Bar / Validate Button / Validation Bar:

2015-09-18_014447

As an example, consider this field where the validations have been set at:

2015-09-18_014607

So exceeding the character limit (after stripping off html tags!), would give a fatal error.

2015-09-18_015123

2015-09-18_015109

, , , , , ,

Leave a comment

Sitecore Image Size Validator

In most sites, by design we have some minimum image size requirements which the content authors need to adhere by while uploading content images to maintain the integrity of the design.

In our project we wanted to add a validation in place so that the content author wouldn’t be allowed to set images smaller than what minimum requirement was set by the front end designers.

In some cases, we also needed to enforce an exact dimensions match, so we included the provision for that as well.

Here’s how we went about this:

We created a new template to use here – Base Image Validation
2015-09-17_233234

And added this new template as a base for the Template Field template:

2015-09-17_233428

So now, these fields would be available on all template fields. Even though these fields will be available in all template fields, the validation logic we hook up, will be fired only for image field types.

So here’s the code we need to hook up the validation logic:

namespace mySite.Customizations.Customized_Sitecore
{
    [Serializable]
    public class ImageSizeValidator : StandardValidator
    {
        public ImageSizeValidator(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {

        }

        public ImageSizeValidator()
        {

        }

        public override string Name
        {
            get { return "ImageSizeValidator"; }
        }

        protected override ValidatorResult Evaluate()
        {
            ItemUri itemUri = ItemUri;

            if (itemUri == null)
                return ValidatorResult.Valid;

            Field field = GetField();
            if (field == null)
                return ValidatorResult.Valid;

            string str1 = field.Value;
            if (string.IsNullOrEmpty(str1) || string.Compare(str1, "&lt;image /&gt;", StringComparison.InvariantCulture) == 0)
                return ValidatorResult.Valid;
            XmlValue xmlValue = new XmlValue(str1, "image");
            string attribute = xmlValue.GetAttribute("mediaid");
            string str2;
            if (!str1.StartsWith("/"))
                str2 = string.Empty;
            else
                str2 = "/sitecore/media library/" + str1.TrimStart(new[]
                {
                    '/'
                });

            string path = str2;

            if (string.IsNullOrEmpty(attribute) &amp;&amp; string.IsNullOrEmpty(path) || !string.IsNullOrEmpty(xmlValue.GetAttribute("alt")))
                return ValidatorResult.Valid;

            Database database = Factory.GetDatabase(itemUri.DatabaseName);
            Assert.IsNotNull(database, itemUri.DatabaseName);
            Item mediaItem = !string.IsNullOrEmpty(attribute) ? database.GetItem(attribute, itemUri.Language) : database.GetItem(path, itemUri.Language);
            Item fieldItem = database.GetItem(field.ID, itemUri.Language);

            if (mediaItem == null || IsValidDimensions(mediaItem, fieldItem))
                return ValidatorResult.Valid;

            Text = GetText("Selected image in the field \"{0}\" does not match the size restrictions "
                + (SitecoreHelper.ItemRenderMethods.GetCheckBoxValueByFieldName("Exact Dimensions Match", fieldItem)
                ? "(Width:{1}, Height:{2})" : "(Minimum Width:{1}, Minimum Height:{2})"), new[]
            {
                field.DisplayName,
                fieldItem.Fields["Minimum Width"].Value,
                fieldItem.Fields["Minimum Height"].Value
            });
            return GetFailedResult(ValidatorResult.FatalError);
        }

        private bool IsValidDimensions(Item mediaItem, Item fieldItem)
        {
            int validationHeight, validationWidth, imageWidth, imageHeight;

            int.TryParse(fieldItem.Fields["Minimum Width"].Value, out validationWidth);
            int.TryParse(fieldItem.Fields["Minimum Height"].Value, out validationHeight);
            int.TryParse(mediaItem.Fields["Width"].Value, out imageWidth);
            int.TryParse(mediaItem.Fields["Height"].Value, out imageHeight);

            if (SitecoreHelper.ItemRenderMethods.GetCheckBoxValueByFieldName("Exact Dimensions Match", fieldItem))
            {
                if ((validationHeight == 0 &amp;&amp; validationWidth == 0)
                || (validationHeight == 0 &amp;&amp; validationWidth &gt; 0 &amp;&amp; validationWidth == imageWidth)
                || (validationHeight &gt; 0 &amp;&amp; imageHeight == validationHeight &amp;&amp; validationWidth == 0)
                || (validationWidth &gt; 0 &amp;&amp; validationHeight &gt; 0 &amp;&amp; validationWidth == imageWidth &amp;&amp; validationHeight == imageHeight))
                {
                    return true;
                }
            }
            else if ((validationHeight == 0 &amp;&amp; validationWidth == 0)
                || (validationHeight == 0 &amp;&amp; validationWidth &gt; 0 &amp;&amp; validationWidth &lt;= imageWidth) || (validationHeight &gt; 0 &amp;&amp; validationHeight &lt;= imageHeight &amp;&amp; validationWidth == 0) || (validationWidth &gt; 0 &amp;&amp; validationHeight &gt; 0 &amp;&amp; validationWidth &lt;= imageWidth &amp;&amp; validationHeight &lt;= imageHeight))
            {
                return true;
            }

            return false;
        }

        protected override ValidatorResult GetMaxValidatorResult()
        {
            return GetFailedResult(ValidatorResult.FatalError);
        }
    }
}

And we then created a new validation rule item in Sitecore and pointed it to this method in our assembly:

2015-09-17_234518

This rule then needs to be added to the image field item, depending upon what actions you would like to trigger the validations at, the rule will need to be added accordingly. Eg. Quick Action Bar / Validate Button / Validation Bar:

2015-09-18_001320

As an example, consider this field where the validations have been set at:

2015-09-18_002701

So uploading an image smaller than this would result in the error state:

2015-09-18_002909

, , , ,

Leave a comment

Safely truncate Sitecore Rich Text / Html

We came across the need to be able to truncate text (html) entered in a rich text field to be able to display the same on compact modules / show article summaries etc.
There are many different approaches that can be taken here to be able to safely truncate html. But here’s the code that passed our testing and is currently live! We are parsing the html into XML to achieve this.

Please note:
This code truncates the text to the nearest word, and if the character limit is reached within the first word, the truncation is done mid word instead of returning no text at all. It will also allow you to optionally append ellipses to the truncated text.

        public static string TruncateHtml(string text, int charCount, bool appendEllipses = false)
        {
            text = HttpUtility.HtmlDecode(text);

            if (charCount <= 0) return text;

            try
            {
                // your data, probably comes from somewhere, or as params to a methodint 
                XmlDocument xml = new XmlDocument();
                xml.LoadXml("" + text + "");
                // create a navigator, this is our primary tool
                XPathNavigator navigator = xml.CreateNavigator();
                XPathNavigator breakPoint = null;

                // find the text node we need:
                while (navigator.MoveToFollowing(XPathNodeType.Text))
                {
                    int remainingCharacters = charCount;
                    charCount -= navigator.Value.Length;
                    if (charCount <= 0) { string lastText = TruncateText(navigator.Value, remainingCharacters) + (StripHtml(text).Length > charCount && appendEllipses ? "..." : "");
                        navigator.SetValue(lastText);
                        breakPoint = navigator.Clone();
                        break;
                    }
                }

                // first remove text nodes, because Microsoft unfortunately merges them without asking
                while (navigator.MoveToFollowing(XPathNodeType.Text))
                {
                    if (navigator.ComparePosition(breakPoint) == XmlNodeOrder.After)
                    {
                        navigator.DeleteSelf();
                    }
                }

                // moves to parent, then move the rest
                navigator.MoveTo(breakPoint);
                while (navigator.MoveToFollowing(XPathNodeType.Element))
                {
                    if (navigator.ComparePosition(breakPoint) == XmlNodeOrder.After)
                    {
                        navigator.DeleteSelf();
                    }
                }

                // moves to parent
                navigator.MoveToRoot();
                navigator.MoveToFollowing(XPathNodeType.Element);
                return navigator.InnerXml;
            }
            catch (Exception)
            {
                return text;
            }
        }

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

            str = HttpUtility.HtmlDecode(str);
            if (str.Length <= maxCharCount || maxCharCount == 0) return str; int originalStrLen = str.Length; if (str.Length > maxCharCount)
                str = str.Substring(0, maxCharCount + 1);

            int 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);
        }

The TruncateText() method only truncates a given string to the nearest word, while the TruncateHtml() method is responsible for converting the passed in text into XML and safely truncating the same, taking care of maintaining valid XML and hence valid HTML.

We have also wrapped any passed in text into an external XML tag , to make sure that passed in free text also can be parsed in this way.

When it came to using this in Sitecore, here’s an example of how we have put this to use!

@Html.Raw(Editable(Model, m => m.Title, x => TextUtilityStatic.TruncateHtml(x.Title, 50, true)))

So this way, the field would still be editable in page editor mode, but output the truncated html on the page!

, , , , ,

Leave a comment