Wednesday 24 February 2016

Extending an Image field in Sitecore (part 1)

On my last project one of the more fun things I got to play with was the SPEAK framework, and today's posts (which will be quite large) will outline how to extend the default Sitecore Image field. We're simply going to add an additional icon which is overlaid on top of our main image (like a watermark), and use a simple SPEAK interface to allow it to be positioned (by being dragged, and setting x,y coordintes in pixels) by the content author.
So, first things first: since we're extending the Image field, let's go into the core database and make a duplicate of that field, which you can find at /sitecore/system/Field types/Simple Types/Image. I'm going to call mine "Image With Overlay". Let's also add a new button to open our SPEAK interface, so expand the Menu folder under the new item, and duplicate the Browse button; call it "Overlay", and change the Display Name field to "Overlay" as well. Now there are 2 more things we need to change: in the "Image With Overlay" item we just created, we need to update the Control field, and put overlay:ImageWithOverlay (we'll get back to this later). In the "Overlay" button item, update the Message field so that "open" is now "overlay". It should look like contentimage:overlay(id=$Target).
Ok now that we've got our field defined in Sitecore, let's add the code side of things. Let's extend the default Sitecore.Data.Fields.ImageField and add a new property called OverlayCoordinates to store the coordinates at which to display our overlay. Hopefully you're familiar with what Sitecore Image fields look like as raw values (if not, turn on raw values and have a look with an image with a custom alt tag and title) - we're going to store our coordinates as a new xml attribute in the same way, and this OverlayCoordinates property does just that.
using Sitecore.Data.Fields;

public class ImageWithOverlayField : ImageField
{
    public ImageWithOverlayField(Field innerField)
        : base(innerField)
    {
    }
    
    public ImageWithOverlayField(Field innerField, string runtimeValue)
        : base(innerField, runtimeValue)
    {
    }
    
    public string OverlayCoordinates
    {
        get
        {
            return GetAttribute(Constants.CoordinatesAttribute) ?? Constants.OverlayDefaultCoordinates;
        }

        set
        {
            SetAttribute(Constants.CoordinatesAttribute, value ?? Constants.OverlayDefaultCoordinates);
        }
    }
    
    public static implicit operator ImageWithOverlayField(Field field)
    {
        return field == null ? null : new ImageWithOverlayField(field);
    }
}
Ok now that we've extended our field to store the coordinates attribute, let's extend the actual class which renders our Image field, and show our overlay over top in the preview. The easiest way is to decompile Sitecore.Shell.Applications.ContentEditor.Image and extend+reuse what we can, and copy+update what we cannot reuse (ie. if it's a private method).
using System;
using System.Linq;
using System.Text;
using System.Web.UI;
using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Resources.Media;
using Sitecore.Shell.Applications.ContentEditor;
using Sitecore.Text;
using Sitecore.Web;
using Sitecore.Web.UI.Sheer;
using Convert = System.Convert;

public class ImageWithOverlay : Image
{
    protected override void DoRender(HtmlTextWriter output)
    {
        Assert.ArgumentNotNull(output, "output");
        Item mediaItem = GetMediaItem();

        string src = GetSrc();
        string str1 = " src=\"" + src + "\"";
        string str2 = " id=\"" + ID + "_image\"";
        string str3 = " alt=\"" + (mediaItem != null ? WebUtil.HtmlEncode(mediaItem["Alt"]) : string.Empty) + "\"";
        string coordinates = XmlValue.GetAttribute(Common.Constants.CoordinatesAttribute);

        if (string.IsNullOrEmpty(coordinates))
        {
            coordinates = Common.Constants.OverlayDefaultCoordinates;
        }

        int[] coords = coordinates.Split(',').Select(int.Parse).ToArray();

        // base.DoRender(output);
        output.Write("
"); output.Write("
"); string dimensions = ""; string overlayDimensions = ""; int[] padding = { 12, 8 }; if (mediaItem != null) { int width = Convert.ToInt32(mediaItem["Width"]); int height = Convert.ToInt32(mediaItem["Height"]); double scale = 128.0 / height; dimensions = "width=\"" + Math.Round(scale * width) + "px\" height=\"" + Math.Round(scale * height) + "px\""; overlayDimensions = "left:" + (Math.Round(coords[0] * scale) + padding[0]) + "px;top:" + (Math.Round(coords[1] * scale) + padding[1]) + "px;"; } else { overlayDimensions = "left:" + padding[0] + "px;top:" + padding[1] + "px;"; } output.Write(""); output.Write(""); output.Write("
"); output.Write("
"); string details = GetDetails(); output.Write(details); output.Write("
"); output.Write("</div>"); } private Item GetMediaItem() { string attribute = XmlValue.GetAttribute("mediaid"); if (attribute.Length <= 0) { return null; } Language language = Language.Parse(ItemLanguage); return Client.ContentDatabase.GetItem(attribute, language); } private string GetSrc() { string src = string.Empty; MediaItem mediaItem = GetMediaItem(); if (mediaItem != null) { MediaUrlOptions thumbnailOptions = MediaUrlOptions.GetThumbnailOptions(mediaItem); int result; if (!int.TryParse(mediaItem.InnerItem["Height"], out result)) { result = 128; } thumbnailOptions.Height = Math.Min(128, result); thumbnailOptions.MaxWidth = 640; thumbnailOptions.UseDefaultIcon = true; src = MediaManager.GetMediaUrl(mediaItem, thumbnailOptions); } return src; } private string GetDetails() { string str1 = string.Empty; MediaItem mediaItem = GetMediaItem(); if (mediaItem != null) { Item innerItem = mediaItem.InnerItem; StringBuilder stringBuilder = new StringBuilder(); XmlValue xmlValue = XmlValue; stringBuilder.Append("
"); string str2 = innerItem["Dimensions"]; string str3 = WebUtil.HtmlEncode(xmlValue.GetAttribute("width")); string str4 = WebUtil.HtmlEncode(xmlValue.GetAttribute("height")); if (!string.IsNullOrEmpty(str3) || !string.IsNullOrEmpty(str4)) { stringBuilder.Append(Translate.Text("Dimensions: {0} x {1} (Original: {2})", str3, str4, str2)); } else { stringBuilder.Append(Translate.Text("Dimensions: {0}", str2)); } stringBuilder.Append("
"); stringBuilder.Append("
"); string str5 = WebUtil.HtmlEncode(innerItem["Alt"]); string str6 = WebUtil.HtmlEncode(xmlValue.GetAttribute("alt")); if (!string.IsNullOrEmpty(str6) && !string.IsNullOrEmpty(str5)) { stringBuilder.Append(Translate.Text("Alternate Text: \"{0}\" (Default Alternate Text: \"{1}\")", str6, str5)); } else if (!string.IsNullOrEmpty(str6)) { stringBuilder.Append(Translate.Text("Alternate Text: \"{0}\"", str6)); } else if (!string.IsNullOrEmpty(str5)) { stringBuilder.Append(Translate.Text("Default Alternate Text: \"{0}\"", str5)); } else { stringBuilder.Append(Translate.Text("Warning: Alternate Text is missing.")); } stringBuilder.Append("
"); stringBuilder.Append("
"); string str7 = WebUtil.HtmlEncode(xmlValue.GetAttribute(Common.Constants.CoordinatesAttribute)); stringBuilder.Append(!string.IsNullOrEmpty(str7) ? Translate.Text("Overlay coordinates: {0}", str7) : Translate.Text("Overlay coordinates: No coordinates set, using 100,100.")); stringBuilder.Append("
"); str1 = stringBuilder.ToString(); } if (str1.Length == 0) { str1 = Translate.Text("This media item has no details."); } return str1; } }
You'll notice the DoRender() method is where we're now including our overlay.png over top of the main image using relative and absolute CSS positioning. I've also added an extra line in the GetDetails() method which shows the user the currently set coordinates, like the "dimensions" and "alt" are currently shown below the image preview.
Ok so now that we've got our classes, let's hook up the Sitecore side of things with the code. For this, we'll need a new include file (which it's always best to put in a "zzz" folder so that it's included last).
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <fieldTypes>
      <fieldType name="ImageWithOverlay" type="SpeakImageOverlay.Models.Fields.ImageWithOverlayField, SpeakImageOverlay" />
    </fieldTypes>
    <controlSources>
      <source mode="on" namespace="SpeakImageOverlay.Models.Controls" assembly="SpeakImageOverlay" prefix="overlay"/>
    </controlSources>
  </sitecore>
</configuration>
The fieldType you'll notice is the ImageWithOverlayField class we created. The source namespace/assembly is where the ImageWithOverlay class lives. Do you remember back to the first step in the core database where we set our Control field to overlay:ImageWithOverlay? Hopefully you'll see now that "overlay" is the source prefix, and "ImageWithOverlay" is the field name.
Ok at this point you should be able to work with your new field in Sitecore! The Overlay button won't do anything, but you should see your overlay image and be able to set the main image as usual. Let's bind everything up using Glass Mapper so we can use it on the frontend as well.
I'm just using a basic Glass Mapper setup, so in GlassMapperScCustom.cs, in CreateResolver() where the container is being created, let's add the line: container.Register(Component.For().ImplementedBy().LifeStyle.Transient);. If you don't have a container set up, have a look into using the Glass.Mapper.Sc.CastleWindsor (make sure it's a compatible version). This will use our ImageOverlayMapper where possible to map our images. The code for this is pretty straightforward. I found it easier to copy some code from SitecoreFieldImageMapper.cs rather than extending it.
public class ImageWithOverlay : Glass.Mapper.Sc.Fields.Image
    {
        public virtual string OverlayCoordinates { get; set; }
    }
using System;
using Fields;
using Glass.Mapper.Sc;
using Glass.Mapper.Sc.Configuration;
using Glass.Mapper.Sc.DataMappers;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;

public class ImageOverlayMapper : AbstractSitecoreFieldMapper
{
    public ImageOverlayMapper()
        : base(typeof(ImageWithOverlay))
    {
    }
    
    public override object GetField(Field field, SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
    {
        ImageWithOverlay img = new ImageWithOverlay();
        ImageWithOverlayField sitecoreImage = new ImageWithOverlayField(field);

        SitecoreFieldImageMapper.MapToImage(img, sitecoreImage);
        img.OverlayCoordinates = sitecoreImage.OverlayCoordinates;

        return img;
    }
    
    public override void SetField(Field field, object value, SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
    {
        ImageWithOverlay img = value as ImageWithOverlay;
        if (field == null || img == null)
        {
            return;
        }

        var item = field.Item;

        ImageWithOverlayField sitecoreImage = new ImageWithOverlayField(field);

        SitecoreFieldImageMapper.MapToField(sitecoreImage, img, item);
        sitecoreImage.OverlayCoordinates = img.OverlayCoordinates;
    }
    
    public override string SetFieldValue(object value, SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
    {
        throw new NotImplementedException();
    }
    
    public override object GetFieldValue(string fieldValue, SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
    {
        Item item = context.Service.Database.GetItem(new ID(fieldValue));

        if (item == null)
        {
            return null;
        }

        MediaItem imageItem = new MediaItem(item);
        ImageWithOverlay image = new ImageWithOverlay();
        SitecoreFieldImageMapper.MapToImage(image, imageItem);
        image.OverlayCoordinates = Constants.OverlayDefaultCoordinates;
        return image;
    }
}
Now we can just use our ImageWithOverlay (the one which extends Glass.Mapper.Sc.Fields.Image) in our model and on our page!
@using Glass.Mapper.Sc.Web.Mvc
@model SpeakImageOverlay.Models.IPage

@{
 string[] coordinates = SpeakImageOverlay.Models.Constants.OverlayDefaultCoordinates.Split(',');
 if (Model != null &amp;&amp; Model.PageImage != null &amp;&amp; !string.IsNullOrEmpty(Model.PageImage.OverlayCoordinates))
 {
 coordinates = Model.PageImage.OverlayCoordinates.Split(',');
 }
}

<div style="position:relative;">
 @Html.Glass().RenderImage(Model, m =&gt; m.PageImage, new { style = "position: absolute; max-width: 100%;" }, true)
 <img src="~/Content/images/overlay.png" style="position:absolute;width:50px;top:@coordinates[1]px;left:@coordinates[0]px;" />
</div>
In the next post I'll run through how to add the SPEAK component to set the coordinates of our overlay image.

No comments:

Post a Comment