Archive for category Glassmapper

EditFrame vs Experience Editor Buttons in Sitecore

EditFrame and Experience editor buttons are both great ways of adding experience editor support to our Sitecore pages.

  • EditFrame buttons
    • Create item in core database (I usually just duplicate the Default folder at /sitecore/content/Applications/WebEdit/Edit Frame Buttons/Default). You can add the field(s) you want to make editable in the ‘Fields’ field
    • In your view, you need to wrap the html that you want to make editable, with the EditFrame code, where you need to supply it with the path of your edit frame button in core, and the item ID of the item you want to edit. The fields made editable would depend on the fields you include in the EditFrame button item as shown in the above image.
      @using (BeginEditFrame("/sitecore/content/Applications/WebEdit/Edit Frame Buttons/Headline", Model.Id.ToString()))
      {
      <div>@Model.Headline</div>
      }
      

    You will now be able to see additional buttons around the html you enclosed in the EditFrame code

  • Experience Editor Buttons
    • Create button item in Core database – use the template – /sitecore/templates/System/WebEdit/Field Editor Button and create the /sitecore/content/Applications/WebEdit/Custom Experience Buttons
    • In your rendering, select the button you just created in the ‘Experience Editor Buttons’ field

    The additional button you created will now be available when you select the entire rendering. If the datasource is available, that item is considered for editing and the field name mentioned in the experience editor button item in core is searched for in that item, else the context item is considered for the same.

Points to note about these 2 implementations:

EditFrame Buttons Experience Editor Buttons
Providing fields to edit Pipe separated – In the core item for the button Pipe separated – In the core item for the button
Item to be edited Item id passed in, in the view code The datasource item is used if available, else the context item is used. This is irrespective of whether the fields mentioned in the button item exist in the datasource item.
Code change required Yes, code needs to be added in the view as shown above. No
Html to be selected  Only the html around which the EditFrame code is wrapped needs to be selected to see the button. The available buttons show up when the entire rendering is selected.
Flexibility  You can choose to edit any number of items selecting any single html tag as is the requirement. Only 1 item can be edited per rendering (datasource or context item), and individual html elements cannot be selected to see the button(s).

, , ,

Leave a comment

Glass html helpers for responsive images in Sitecore with bLazy

The bLazy Plugin provides us with a great way to implement serving up resized images based on the current viewport, and additionally it also allows us to lazy load images on our site, drastically reducing the page size and download time.

We used bLazy for img tags – and also for other tags (where it simply loads up the image as a background image).

We created a few glass HTML helpers here to help us maintain consistency and increase maintainability by having this html present in only 1 place in the site.

        public HtmlString RenderResponsiveLazyImage(Expression<Func<TModel, object>> field,
            Dimensions mobileDimensions, Dimensions tabletDimensions, Dimensions desktopDimensions, 
            bool isEditable = true, NameValueCollection parameters = null)
        {
            return RenderResponsiveLazyImage(Model, field, mobileDimensions, tabletDimensions, desktopDimensions, isEditable, parameters);
        }

        public HtmlString RenderResponsiveLazyImage<T>(T model, Expression<Func<T, object>> field, 
            Dimensions mobileDimensions, Dimensions tabletDimensions, Dimensions desktopDimensions,
            bool isEditable = true, NameValueCollection parameters = null)
        {
            Image imageField = field.Compile().Invoke(model) as Image;

            if (imageField != null)
            {
                StringBuilder responsiveImageHtml = new StringBuilder();
                StringBuilder parametersHtml = new StringBuilder();
                if (parameters != null)
                {
                    var items = parameters.AllKeys.SelectMany(parameters.GetValues, (k, v) => new { key = k, value = v });
                    foreach (var item in items)
                    {
                        parametersHtml.Append(" " + item.key + "=\"" + item.value + "\"");
                    }
                }

                if (Sitecore.Context.PageMode.IsPageEditor)
                {
                    responsiveImageHtml.Append(GlassHtml.RenderImage(model, field, null, isEditable));
                }
                else if (!Sitecore.Context.PageMode.IsPageEditor && imageField.IsValid())
                {
                    responsiveImageHtml.Append("<img" + (parametersHtml.Length == 0 ? "" : parametersHtml.ToString())
                        + " src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\""
                        + " data-lazily-respond-mobile=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, 
                            mobileDimensions != null ? mobileDimensions.Width : 0, mobileDimensions != null ? mobileDimensions.Height : 0) + "\""
                        + " data-lazily-respond-tablet=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, 
                            tabletDimensions != null ? tabletDimensions.Width : 0, tabletDimensions != null ? tabletDimensions.Height : 0) + "\""
                        + " data-lazily-respond-desktop=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, 
                            desktopDimensions != null ? desktopDimensions.Width : 0, desktopDimensions != null ? desktopDimensions.Height : 0) + "\""
                        + " alt=\"" + imageField.Alt + "\" />");
                }

                return new HtmlString(responsiveImageHtml.ToString());
            }

            return new HtmlString(string.Empty);
        }

        public string RenderResponsiveLazyImageAttributes(Expression<Func<TModel, object>> field,
            Dimensions mobileDimensions, Dimensions tabletDimensions, Dimensions desktopDimensions)
        {
            return RenderResponsiveLazyImageAttributes(Model, field, mobileDimensions, tabletDimensions, desktopDimensions);
        }

        public string RenderResponsiveLazyImageAttributes<T>(T model, Expression<Func<T, object>> field,
            Dimensions mobileDimensions, Dimensions tabletDimensions, Dimensions desktopDimensions)
        {
            Image imageField = field.Compile().Invoke(model) as Image;

            if (imageField != null && imageField.IsValid())
            {
                StringBuilder responsiveLazyImageHtml = new StringBuilder();

                if (mobileDimensions != null)
                {
                    responsiveLazyImageHtml.Append("data-lazily-respond-mobile=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, mobileDimensions.Width, mobileDimensions.Height) + "\"");
                }

                if (tabletDimensions != null)
                {
                    responsiveLazyImageHtml.Append(" data-lazily-respond-tablet=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, tabletDimensions.Width, tabletDimensions.Height) + "\"");
                }

                if (desktopDimensions != null)
                {
                    responsiveLazyImageHtml.Append(" data-lazily-respond-desktop=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, desktopDimensions.Width, desktopDimensions.Height) + "\"");
                }

                return responsiveLazyImageHtml.ToString();
            }
            return string.Empty;
        }

        public static string GetResizedSitecoreUrl(string imageUrl, int width, int height, bool centerCrop = true)
        {
            if (string.IsNullOrWhiteSpace(imageUrl)) return string.Empty;

            if (height > 0)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&h=" : "?h=") + height;
            if (width > 0)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&w=" : "?w=") + width;
            if (centerCrop)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&" : "?") + "usecustomfunctions=1&centercrop=1";

            return imageUrl;
        }

We used Sitecore Image Processor Module to be able to resize sitecore images here.

Where Dimensions is:

    public class Dimensions
    {
        public int ScreenSize { get; set; }
        public int Width { get; set; }
        public int Height { get; set; }
        public bool IsUrlBlank { get; set; }

        public Dimensions(int width, int height)
        {
            Width = width;
            Height = height;
        }

        public Dimensions()
        { }
    }

, , , , , , ,

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

Render Responsive Image Glass Html Helper with Sitecore Image Processor Module

In today’s world of multi-sized devices / screens, responsive design is almost a must for trendy websites.
The <picture> tag provides a nice solution to the need for serving up resized / different images based on the viewport / orientation of the client screen.

Responsive

I found this article very useful for reading up on the <picture> tag.
http://webdesign.tutsplus.com/tutorials/quick-tip-how-to-use-html5-picture-for-responsive-images–cms-21015

This allows us to efficiently use bandwidth and serve up appropriately sized images based on the screen size AND pixel density. In addition, we could also use different images altogether based on the viewport.

In one of our Sitecore projects we extensively used picture tags. We used it in tandem with the Sitecore Image Processor Module to resize images based on view port. In our solution, we didn’t make any changes based on screen orientation (which is definitely an option too in the picture tag). We served up differently sized versions of the same image based on the viewport.

We created a glass html helper method to help render the picture tag for a given sitecore item field, passing in the max size of the image per breakpoint.

        public HtmlString RenderResponsivePicture<T>(T model, Expression<Func<T, object>> field, List<Dimensions> dimensions,
            bool isEditable = false, NameValueCollection parameters = null, bool crop = true)
        {
            Image imageField = field.Compile().Invoke(model) as Image;

            if (imageField != null)
            {
                if (Sitecore.Context.PageMode.IsPageEditor)
                {
                    return new HtmlString(GlassHtml.RenderImage(model, field, null, isEditable));
                }
                if (imageField.IsValid())
                {
                    StringBuilder responsiveImageHtml = new StringBuilder();
                    StringBuilder parametersHtml = new StringBuilder();
                    if (parameters != null)
                    {
                        var items = parameters.AllKeys.SelectMany(parameters.GetValues, (k, v) => new { key = k, value = v });
                        foreach (var item in items)
                        {
                            parametersHtml.Append(" " + item.key + "=\"" + item.value + "\"");
                        }
                    }

                    responsiveImageHtml.Append("<picture" + (parametersHtml.Length == 0 ? "" : parametersHtml.ToString()) + ">");

                    if (dimensions != null && dimensions.Count() > 1)
                    {
                        foreach (Dimensions param in dimensions.Take(dimensions.Count - 1))
                        {
                            responsiveImageHtml.Append
                                (string.Format(
                                    "<!--[if IE 9]><video style='display: none;'><![endif]--><source media=\"(min-width: {0}px)\" srcset=\"{1}\"><!--[if IE 9]></video><![endif]-->",
                                    param.ScreenSize, param.IsUrlBlank ? string.Empty : ImageHelper.GetResizedSitecoreUrl(imageField.Src, param.Width, param.Height, crop)));
                        }
                        Dimensions lastparam = dimensions.Last();
                        responsiveImageHtml.Append(string.Format("<img srcset=\"{0}\" alt=\"{1}\">",
                            ImageHelper.GetResizedSitecoreUrl(lastparam.IsUrlBlank ? string.Empty : imageField.Src, lastparam.Width, lastparam.Height), imageField.Alt));
                    }
                    responsiveImageHtml.Append("</picture>");

                    return new HtmlString(responsiveImageHtml.ToString());
                }
            }

            return new HtmlString(string.Empty);
        }

        public HtmlString RenderResponsivePicture(Expression<Func<TModel, object>> field,
            List<Dimensions> dimensions, bool isEditable = false, NameValueCollection parameters = null, bool crop = true)
        {
            return RenderResponsivePicture(Model, field, dimensions, isEditable, parameters, crop);
        }

You’ll note that we have an additional line in there to allow for page editor support! So in page editor mode, we would render this as a regular img tag.
Additionally, we had multiple instances where the design called for not showing a particular image in the mobile view at all. To support this, we also added the ability to pass in a token which signifies that a given breakpoint url needs to be blank.

Please note, the List takes the dimensions in the order: Desktop –> Tab –> Mobile.
You could ofcourse choose to be more explicit and have different parameters for each breakpoint.

Where Dimensions is an internal class:

    public class Dimensions
    {
        public int ScreenSize { get; set; }
        public int Width { get; set; }
        public int Height { get; set; }
        public bool IsUrlBlank { get; set; }

        public Dimensions(int width, int height)
        {
            Width = width;
            Height = height;
        }

        public Dimensions()
        { }
    }

In our project, we also had to support IE, so we used PictureFill as well, and you’ll see that the output html also outputs a video tag around the picture tag for IE9 as is needed to support the browser.

Additionally, we also had to render certain images on our site, which were not a part of a sitecore field. A good example of this, would be using an existing image repository for say product images, or user uploaded images which we store in a different location. For this we utilized the following code, which takes in an image url and a set of dimensions and builds the picture tag for you:

        public HtmlString RenderResponsiveNonFieldPicture(string url, List<Dimensions> dimensions,
            string altText = null, NameValueCollection parameters = null)
        {
            if (string.IsNullOrWhiteSpace(url))
            {
                return new HtmlString(string.Empty);
            }

            StringBuilder responsiveImageHtml = new StringBuilder();
            StringBuilder parametersHtml = new StringBuilder();
            if (parameters != null)
            {
                var items = parameters.AllKeys.SelectMany(parameters.GetValues, (k, v) => new { key = k, value = v });
                foreach (var item in items)
                {
                    parametersHtml.Append(" " + item.key + "=\"" + item.value + "\"");
                }
            }

            responsiveImageHtml.Append("<picture" + (parametersHtml.Length == 0 ? "" : parametersHtml.ToString()) + ">");
            if (dimensions != null && dimensions.Count() > 1)
            {
                foreach (Dimensions param in dimensions.Take(dimensions.Count - 1))
                {
                    responsiveImageHtml.Append
                        (string.Format("<!--[if IE 9]><video style='display: none;'><![endif]--><source media=\"(min-width: {0}px)\" srcset=\"{1}\"><!--[if IE 9]></video><![endif]-->",
                        param.ScreenSize, param.IsUrlBlank ? string.Empty : ImageHelper.GetResizedExternalUrl(url, param.Width, param.Height)));
                }
                Dimensions lastparam = dimensions.Last();
                responsiveImageHtml.Append(string.Format("<img srcset=\"{0}\" alt=\"{1}\">",
                    ImageHelper.GetResizedExternalUrl(lastparam.IsUrlBlank ? string.Empty : url, lastparam.Width, lastparam.Height), altText ?? string.Empty));
            }
            responsiveImageHtml.Append("</picture>");

            return new HtmlString(responsiveImageHtml.ToString());
        }

There are 2 underlying methods here which actually build the url for the resized images. For images in Sitecore, we used the Sitecore Image Processor Module. Going through the documentation, you’ll see that all we need to do is add the querystring parameter ‘usecustomfunctions’ to kick off the Image processor module resizing. So here’s the method we used:

public static string GetResizedSitecoreUrl(string imageUrl, int width, int height, bool centerCrop = true)
        {
            if (string.IsNullOrWhiteSpace(imageUrl)) return string.Empty;

            if (height > 0)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&h=" : "?h=") + height;
            if (width > 0)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&w=" : "?w=") + width;
            if (centerCrop)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&" : "?") + "usecustomfunctions=1&centercrop=1";

            return imageUrl;
        }

For external images, we ended up writing out own code to resize images. You’ll see, we also added an additional control to determine the quality of the resized image. You could also choose to build out your own image resizing module with this code.
We used a http handler for this, and made it asynchronous, so that the image resizing happens asynchronously!

So the method we use to get the resized url basically created a url to the http handler, passing in the width / height parameters:

        public static string GetResizedExternalUrl(string url, int width, int height, bool crop = true)
        {
            if (string.IsNullOrWhiteSpace(url))
            {
                return url;
            }

            return HttpUtility.HtmlDecode(Constants.Url.ImageResizer + "?path=" + HttpUtility.HtmlEncode(url)
                + "&w=" + width) + "&h=" + height + "&crop=" + (crop ? "true" : "false"));
        }

In the http handler, we do the actual image resizing, which you can see here: Resize Images with Crop and Quality Control.

, , , , , , ,

1 Comment

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 &reg; and &trade; 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

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

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