Wednesday 16 March 2016

Custom Sitecore Fields Part 1 of 2

Custom Fields - Part 1 - A Google Maps Field

Abstract 

One of the best features of Sitecore (in my opinion) is how extensible it is.  Almost every aspect of the CMS can be extended, overwritten or enhanced.  This includes fields.

In this two part  blog I am going to cover some different types of fields you can create and how to put them together.

All of the examples and screenshots will be taken from Sitecore version 8.1, but the fields themselves should work in 7.x as well.

We'll start by looking at how you add a custom field to Sitecore, then move on to two  examples of different types of custom fields and finally show you how implement the fields in Sitecore templates, etc.

This article assumes that you have some basic knowledge of Sitecore development and that you are able to create a new Class Library project in Visual Studio and reference all the the necessary Sitecore dlls (perhaps create a nuGet package with all the Sitecore dlls, so that you can import those straight into your project).

Setting up Sitecore

To add a custom field to Sitecore, you need to navigate to the core database.  

From the dashboard click on the desktop icon:

















Once in the desktop, click on the databases icon bottom right and select core.

Then click on the Sitecore icon bottom left, and from the menu click on Content Editor:

























You should now see a content tree similar to the above.

Navigate to /sitecore/system/Field types.

I recommend creating your own field types sub-folder, and putting your fields in there, that way they will be all grouped together when you come to add them to your templates.

To do this: right-click on the field types node and click on insert and then select folder - give it a meaningful name.

Navigate to your newly created folder and right click on the node, select insert and then select Insert from template.  You need to add an item of the following template type: /sitecore/templates/System/Templates/Template field type.

Once added, the only fields we are interested in are the Assembly and Class fields, these will be filled out once we have created the code for our new field in Visual Studio.

Creating a Google Maps Field

This custom field will enable us to display a Google Maps image on a template and configure it to pick information from an address field on the same item.  This could be used by a content author to verify address information being added to an item.

Here's an example of it being used to show the location of a partner, based on the Partner Address field on the same item:

















To start lets create a new class called GoogleMapField.

It needs to inherit from: Sitecore.Web.UI.HtmlControls.Control, and needs to implement:
Sitecore.Shell.Applications.ContentEditor.IContentField

 
public class GoogleMapField : Control, IContentField
{
}

We need to add some properties which will get populated at run time.


public class GoogleMapField : Control, IContentField
{
     //Constants
     private const int MAP_HEIGHT = 300;
     private const int MAP_WIDTH = 600;
     private const string CONTENT_EDITOR_SAVE = "contenteditor:save";
     private const int DEFAULT_ZOOM = 14;
     private const string OVERRIDEZOOM = "OverrideZoom";
     private const string ISDEBUG = "Debug";
     private const string ADDRESSFIELDNAME = "AddressField";

     private Image _mapImageCtrl;

     private int _mapZoomFactor = DEFAULT_ZOOM;
     private bool Debug { get; set; }

     //Needed for a custom field 
     public string Source { get; set; }
     public string ItemID { get; set; }

     //Parameter properties
     protected int OverrideZoom { get; private set; }
     protected string AddressFieldName { get; private set; }

}

Next we'll create a method to parse the parameters  that will be passed into it from the source field in the template where it is used:



In this example the source field parameters are: &Debug=false&AddressField=Partner Address.
  • Debug [Optional] - turns on off display of the of the Google URL above the map.
  • AddressField [Required] - Name of the field containing the address information
  • OverrideZoom [Optional] - An integer value used to override the default zoom level of 14 

private void ParseParameters(string source)
{
     if (string.IsNullOrEmpty(source))
     {
         return;
     }

     var parameters = new UrlString(source);
     if (!string.IsNullOrEmpty(parameters.Parameters[OVERRIDEZOOM]))
     {
         int zoomValue;
         int.TryParse(parameters.Parameters[OVERRIDEZOOM], out zoomValue);

         if (zoomValue < 0 || zoomValue > 20) zoomValue = DEFAULT_ZOOM;

         OverrideZoom = zoomValue;
      }

      if (!string.IsNullOrEmpty(parameters.Parameters[ADDRESSFIELDNAME]))
      {
          AddressFieldName = parameters.Parameters[ADDRESSFIELDNAME];
      }

      if (!string.IsNullOrEmpty(parameters.Parameters[ISDEBUG]))
      {
           Debug = MainUtil.GetBool(parameters.Parameters[ISDEBUG], false);
      }
}
 
Next we need to add a method that extracts the address information and parses it into a format that we can push to Google: 

private string GetGeoData(Item item, ref string centre)
{
       var location = "";

       location = item[AddressFieldName].Replace("
", ", ");

       location = HttpUtility.UrlEncode(location);

       if (string.IsNullOrEmpty(location))
       {
           centre = location;
       }
       return location;
}
Next we need to add a method that will generate the URL:

private string GetGoogleMapImageUrl(string location, string centre)
{
    return
    string.Format(
          !string.IsNullOrEmpty(location)
               ? "http://maps.googleapis.com/maps/api/staticmap?&q={0}&zoom={1}&size={2}x{3}&sensor=false&maptype=roadmap&markers=color:blue%7Clabel:1%7C{0}"
               : "http://maps.googleapis.com/maps/api/staticmap?center={4}&zoom={1}&size={2}x{3}&sensor=false&maptype=roadmap&markers=color:blue%7Clabel:1%7C{0}",
               location, _mapZoomFactor, MAP_WIDTH, MAP_HEIGHT, centre);
}

Finally we override the Render method to put it all together:

protected override void Render(HtmlTextWriter output)
 {
     if (!string.IsNullOrEmpty(Value))
     {
          int.TryParse(Value, out _mapZoomFactor);
     }

     ParseParameters(Source);
     if (!string.IsNullOrEmpty(AddressFieldName))
     {
          base.Render(output);

          var db = Factory.GetDatabase("master") ?? Factory.GetDatabase("web");

          var id = !string.IsNullOrEmpty(ItemID)
               ? ItemID
               : ControlAttributes.Substring(ControlAttributes.IndexOf("//master/", StringComparison.Ordinal) + 9,
                 38);
          var item = new ID(id).ToSitecoreItem(db);

          //render other control

           var centre = "";
     
           var location = GetGeoData(item, ref centre);


           _mapImageCtrl = new Image
           {
               ID = ID + "_Img_MapView",
               CssClass = "imageMapView",
               Width = MAP_WIDTH,
               Height = MAP_HEIGHT,
               ImageUrl = GetGoogleMapImageUrl(location, centre)
            };
            _mapImageCtrl.Style.Add("padding-top", "5px");

            if (Debug)
            {
                output.Write(
                    "<div style="background-color: silver; color: white;">{0}</div>",_mapImageCtrl.ImageUrl);
            }

           _mapImageCtrl.RenderControl(output);
      }
}

What about the buttons? - For those we need to override the HandleMessage method:

public override void HandleMessage(Message message)
{
    if (!string.IsNullOrEmpty(Value))
    {
        int.TryParse(Value, out _mapZoomFactor);
    }

    string messageText;
    if ((messageText = message.Name) == null)
    {
        return;
    }

    if (messageText.Trim() == "contentfile:zoom")
    {
        _mapZoomFactor = _mapZoomFactor + 1 > 20 ? _mapZoomFactor : _mapZoomFactor + 1;
        Value = _mapZoomFactor.ToString();
        Sitecore.Context.ClientPage.Dispatch(CONTENT_EDITOR_SAVE);
        return;
    }

    if (messageText == "contentfile:zoom-")
    {
        _mapZoomFactor = _mapZoomFactor - 1 < 0 ? _mapZoomFactor : _mapZoomFactor - 1;
        Value = _mapZoomFactor.ToString();
        Sitecore.Context.ClientPage.Dispatch(CONTENT_EDITOR_SAVE);
        return;
    }

    if (messageText == "contentfile:reset")
    {
        _mapZoomFactor = _mapZoomFactor - 1 < 0 ? _mapZoomFactor : _mapZoomFactor - 1;
        Value = DEFAULT_ZOOM.ToString();
        Sitecore.Context.ClientPage.Dispatch(CONTENT_EDITOR_SAVE);
        return;
    }
    base.HandleMessage(message);
}

Finishing off Sitecore Field Setup

Now that we have created our Google Maps field, lets go back to Sitecore and finish setting up the field.
















As  mentioned before fill out the Assembly and Class fields with the appropriate information taken from your solution.

Next we need to add the three buttons:



















You need to fill out the following for each of the buttons:
  • The Display Name - Text you want to appear in Sitecore
  • Message  - This should match the Message Text value used in the HandleMessage method above.  You should also add the (id=$Target) to tell Sitecore to target the current item. 
  • Show in Field Editor (not shown) - This should be checked.

 

How could we enhance this?

For one project I worked on, we imported a geo database for North America into Sitecore.  We added the Google Maps Field to the template used to store the geo data items.  I then added some enhancements to enable the Google Maps Field to try and get the information from different fields on the item:
  • Lat & Long
  • Post Code
  • Full Address
private static string GetGeoData(BaseItem item,  ref string centre)
{
    double lat;
    double lng;
    double.TryParse(item["latitude"], out lat);
    double.TryParse(item["longitude"], out lng);
    var location = string.Format("{0},{1}", lat, lng);
            
    if (lat == 0.00 && lng == 0.00)
    {
         location = item["postcode"];
    }
    if (string.IsNullOrEmpty(location))
    {
         centre = string.Format("{0}{1}{2}{3}{4}{5}",
             !string.IsNullOrEmpty(item[LOCALITY]) ? item[LOCALITY] + "," : ""
             , !string.IsNullOrEmpty(item[REGION4]) ? item[REGION4] + "," : ""
             , !string.IsNullOrEmpty(item[REGION3]) ? item[REGION3] + "," : ""
             , !string.IsNullOrEmpty(item[REGION2]) ? item[REGION2] + "," : ""
             , !string.IsNullOrEmpty(item[REGION1]) ? item[REGION1] + "," : ""
             , !string.IsNullOrEmpty(item[COUNTRY]) ? item[COUNTRY] : "");
    }
    return location;
 }

Hopefully this will give you some ideas on how you too, could use this field!

I have the full source (including the Sitecore Package) here if you would like to download it: Sitecore Fields

In the next article we will look at a more complex field and also how to use these fields in Sitecore.