Tuesday, September 25, 2012

Custom field type for the Sitecore Web Forms for Marketers Module

In my humble opinion, the Web Forms for Marketers module does an awesome job of making not-so-trivial processes like collecting and reporting on data, or tagging and emailing users rather trivial. If you've ever had to make a form with 100+ input fields (and their respective labels and validation expressions), I'm sure you would appreciate it, too.

So while I'm on the custom field type theme, I'll share my experience with customizing a field 'help' text to be clickable and open a popup. This was inspired by the "What's this?" links next to CVV input fields in payment forms where a tidy popup shows you a picture of a credit card with a circled verification code.

As with any custom field, the first step is to decide which of the already existing fields you want to extend. For this example, I took the SingleLineText field and created a SingleLinePopupField. You would need to add references to Sitecore.Forms.Core.dll and Sitecore.Forms.Custom.dll to the project.

[Designer("System.Windows.Forms.Design.ParentControlDesigner, System.Design", typeof(IDesigner))]
    public class SingleLinePopupField : SingleLineText
    {
        public SingleLinePopupField()
            : this(HtmlTextWriterTag.Div)
        {
        }
        public SingleLinePopupField(HtmlTextWriterTag tag)
            : base(tag)
        {
        }
        protected override void DoRender(HtmlTextWriter writer)
        {
            base.DoRender(writer);
        }
    }

Create a new field item under /sitecore/system/Modules/Web Forms for Marketers/Settings/Field Types/Custom/ and fill out the Assembly and Class fields. Once this is set up, you should be able to easily create a new form and add a field of your custom type.


In order to have custom properties appear in the Form Designer when you select a form field, you would need to add custom properties to the new field class. For my specific requirements, I added a link type (media library image, video, or external link that will open in a new window), link text, and the actual link.

        [VisualCategory("Custom Properties")]
        [VisualFieldType(typeof(LabelTypeField)), VisualProperty("Popup Link Type", 99), DefaultValue("{9975237B-B750-4A17-86C7-48D5E6D58587}")]
        public string LabelLinkType { get; set; }

        [VisualCategory("Custom Properties")]
        [VisualFieldType(typeof(TextAreaField)), VisualProperty("Popup Link Text", 99), DefaultValue("What's this?")]
        public string LabelLinkText { get; set; }

        [VisualCategory("Custom Properties")]
        [VisualFieldType(typeof(TextAreaField)), VisualProperty("Popup Link", 99), DefaultValue("")]
        public string LabelLink { get; set; }

The VisualCategory attribute will group the custom fields in the Form Designer. VisualProperty defines the field label and sort order, and DefaultValue defines..how many cucumbers are sold obviously.

I used a custom VisualFieldType to create a drop down of link types. I added the custom link types in a new enumeration under /sitecore/system/Modules/Web Forms for Marketers/Settings/Meta data similar to other WFFM enumerations. The custom field type should inherit from WebControl and implement IVisualFieldType


 public class LabelTypeField : WebControl, IVisualFieldType
    {
        public LabelTypeField(): base(HtmlTextWriterTag.Select.ToString()) { }

        public string DefaultValue { get; set; }

        public string EmptyValue { get; set; }

        public bool IsCacheable
        {
            get { return true; }
        }

        public bool Localize { get; set; }

        public ValidationType Validation { get; set; }

        protected virtual void OnPreRender(object sender, EventArgs ev)
        {
            this.Controls.Clear();
            base.OnPreRender(ev);
            //Configuration.LabelLinkTypesRoot is the ID of the enumeration folder item
            foreach (Item type in StaticSettings.ContextDatabase.GetItem(Configuration.LabelLinkTypesRoot).Children)
            {
                string str = type.ID.ToShortID().ToString();
                Literal literal2 = new Literal();
                literal2.Text = string.Format("<option {0} regex='{4}' value='{1}' title='{2}'>{3}</option>", new object[] { (DefaultValue == type.ID.ToString()) ? "selected='selected'" : string.Empty, str, type.DisplayName, type.DisplayName, HttpContext.Current.Server.UrlEncode(type.Fields[FieldIDs.MetaDataListItemValue].Value) });
                Literal child = literal2;
                this.Controls.Add(child);
            }
            base.Attributes["onblur"] = string.Format("Sitecore.PropertiesBuilder.onSavePredefinedValidatorValue('{0}', '{1}')", StaticSettings.prefixId + (Localize ? StaticSettings.prefixLocalizeId : string.Empty), this.ID);
            base.Attributes["onchange"] = base.Attributes["onblur"];
            base.Attributes["onkeyup"] = base.Attributes["onblur"];
            base.Attributes["onpaste"] = base.Attributes["onblur"];
            base.Attributes["oncut"] = base.Attributes["onblur"];
            base.Attributes["class"] = "scFbPeValueProperty";
            base.Attributes["value"] = DefaultValue;
        }

        public string Render()
        {
            this.OnPreRender(this, null);
            StringWriter writer = new StringWriter();
            HtmlTextWriter writer2 = new HtmlTextWriter(writer);
            this.RenderControl(writer2);
            return writer2.InnerWriter.ToString();
        }
    }

Now that all properties are set up, the Form Designer should look something like this:


All that's left is to render the proper html for the custom field to achieve the popup effect. You could actually use any modal popup or video player you wish. I find the ColorBox plugin very lightweight and easy to implement, so it's one of my favorites. The markup it requires is minimal as long as it's integrated and initialized properly. For my example, I included the necessary resource files onto the same layout where the WFFM form placeholder sits, and I defined css classes to initialize the popup for the different types of links. Thus, in the custom field type I would need to override the DoRender() method to render a link with the respective class and attach it to the base 'help' field value:

protected override void DoRender(HtmlTextWriter writer)
{
   if (!string.IsNullOrEmpty(this.LabelLinkText))
   {
       string help = string.Empty;

       if (this.LabelType == LabelLinkTypes.ImagePopup)
       {
           help = string.Format("{0} <a class=\"popup\" href=\"{1}\"><span style=\"font-size:11px; color:gray;\" >{2}</span></a>", this.Information, this.LabelLinkUrl, this.LabelLinkText);
       }
       else if (this.LabelType == LabelLinkTypes.VideoPopup)
       {
           help = string.Format("{0} <a class=\"iframe\" href=\"{1}\"><span style=\"font-size:11px; color:gray;\" >{2}</span></a>", this.Information, this.LabelLinkUrl, this.LabelLinkText);
       }
       else if (this.LabelType == LabelLinkTypes.NewPage)
       { 
           help = string.Format("{0} <a href=\"{1}\" target=\"_blank\"><span style=\"font-size:11px; color:gray;\" >{2}</span></a>", this.Information, this.LabelLink, this.LabelLinkText);
       }
       this.Information = help;
   }

base.DoRender(writer);
}

//Sitecore.Data.ID of the chosen drop down type item
public ID LabelType
{
   get
   {
      return new ID(LabelLinkType);
   }
}

//It might be easier for an editor to input the path to a media item instead of its url, so we'll try for that
public string LabelLinkUrl
{
   get
   {
       string url = string.Empty;

       if (this.LabelType == LabelLinkTypes.ImagePopup)
       {
          //media item
          Item item = StaticSettings.ContextDatabase.GetItem(LabelLink);
          if (item != null)
          {
             MediaItem mi = (MediaItem)item;
             url = MediaManager.GetMediaUrl(mi);
          }
       }
       else
       {
           url = LabelLink;
       }
       return url;
   }
}

And that was it! The links will now be appearing next to the 'help' message below the single-line text field.



Wednesday, September 5, 2012

A custom auto-complete field type for external data in the Sitecore editor

For one of our recent projects, I had to implement a custom Sitecore field that would use a web service as a data source. John West's post on doing this as a DropDown field was extremely helpful. For a basic how-to on creating custom fields, visit this SDN link.

In my scenario, the service could potentially return thousands of items, so a simple drop-down field could easily become painful to use. Using autocomplete became a requirement. I decided to go with jQuery's autocomplete plugin. Using jQuery within the Content Editor is a bit tricky, but definitely doable. An awesome example of custom fields that make use of jQuery is the FieldTypes shared source module.

The custom scripts need to be added to the content editor before anything else is rendered. Use the <renderContentEditor> pipeline to add a processor (or use a config patch file with a patch:before="*[1]" attribute). The processor should look something like this:

public void Process(PipelineArgs args)
{
  if (!Context.ClientPage.IsEvent)
  {
    HttpContext current = HttpContext.Current;
    if (current != null)
    {
     Page handler = current.Handler as Page;
     if (handler != null) {
     Assert.IsNotNull(handler.Header, "Content Editor <head> tag is missing runat='value'");
     handler.Header.Controls.Add(new LiteralControl("<script type='text/javascript' language='javascript' src='/sitecore/shell/custom/autocomplete.js'></script>"));
      }
    }
  }
}

For the actual custom field class, I decided to go with inheriting from the Sitecore.Web.UI.HtmlControls.Control and stick closely to what a regular DropList field would do. The data source of the custom field would contain three parameters: the url of the service to call, the field that would be used as a key, and the field that would be used as the display text of a service "item".

public Dictionary<string, string> ControlParameters = new Dictionary<string, string>() 
{
  {"serviceurl", string.Empty},
  {"textfield", string.Empty},
  {"keyfield", string.Empty}
};
private void LoadControlParameters()
{
  var parameters = Sitecore.StringUtil.ParseNameValueCollection(this.Source, '|', ':');
  foreach (string p in parameters.AllKeys)
    {
      ControlParameters[p] = parameters[p];
    }
}

The service I was calling used json by default, so I decided that the field would support json response and used System.Json.JasonValue for parsing the data:
protected virtual Dictionary<string, string> GetItems()
{
  LoadControlParameters();
  Dictionary<string, string> items = new Dictionary<string, string>();
  try
    {
      // Call the service
      string serviceResult = Sitecore.Web.WebUtil.ExecuteWebPage(ControlParameters["serviceurl"]);

      dynamic json = JsonValue.Parse(serviceResult);

      foreach (dynamic item in json)
      {
        items.Add(item[ControlParameters["keyfield"]].Value.ToString(), item[ControlParameters["textfield"]].Value.ToString());
      }
    }
  catch
    {
      // invalid endpoint
      Sitecore.Diagnostics.Log.Error(string.Format("{0}: Service End-Point Not Found - {1}", this.ToString(), ControlParameters["serviceurl"]), this);
    }

   return items;
}
private Dictionary<string, string> _suggestions;
public Dictionary<string, string> Suggestions
{
  get
  {
    if (_suggestions == null)
    {
      _suggestions = GetItems();
    }
    return _suggestions;
  }
}

Now that we have the key value pairs of suggestions for the field, all that's left is to override the DoRender method of the base Control.

protected override void DoRender(HtmlTextWriter output)
{
  string err = null;
  //check if the data source of the field is empty first
  if (string.IsNullOrEmpty(this.Source))
  {
    err = SC.Globalization.Translate.Text("Source is not defined for this field.");
  }
  else
  {
    //check if the suggestions contain a previously saved value
    //we want to show the value even if it is not returned by the service anymore
    bool found = Suggestions.ContainsKey(this.Value);

    //add any custom css for the field
    output.Write("<link rel='stylesheet' href='/sitecore/shell/custom/autofill.css' />");

    List<string> items = Suggestions.Select(a => string.Format("{0}|{1}", a.Value.Replace("'", string.Empty), a.Key)).ToList();
 
    //output the script for the autocomplete plugin
    string scr = @"
            <script>
                $sc(function () {
                    var availableTags = [
                    '" + String.Join("', '", items.ToArray()) + @"'    
                    ];
                    $sc('#au_{ID}').autocomplete({
                        source: availableTags, 
                        mustMatch: true,
                        focus: function(event, ui) {
                            $sc('#au_{ID}').val(ui.item.value.split('|')[0]);
                            return false;
                        },
                        select: function( event, ui ) {
                $sc('#{ID}').val(ui.item.value.split('|')[1]);
                            return false;
                        }
                    });
                });
         </script>
     <input type='text' class='scContentControl' id=au_{ID}".Replace("{ID}", this.ID) + @" value='{Value}'/>".Replace("{Value}", found ? Suggestions[this.Value] : this.Value);
     output.Write(scr);

     output.Write("<input type='hidden' value='" + this.Value + "' "+ this.GetControlAttributes()+ " />");
     
     //give the user any information that may be important           
     if (Suggestions.Count() == 0)
     {
       err = Sitecore.Globalization.Translate.Text("The service did not return any options.");
     }
     else if (!found && !string.IsNullOrEmpty(this.Value))
     {
       err = Sitecore.Globalization.Translate.Text("Value not in the selection list.");
     }
  }
  if (err != null)
  {
    output.Write("<div style=\"color:#999999;padding:2px 0px 0px 0px\">{0}</div>", err);
  }
}

The $sc variable is what I found Sitecore was overriding the $-function with. This was implemented for and tested on Sitecore 6.5.0.110818, and I can't be sure if the same variable will work with previous Sitecore versions. You can override it with your own variable by calling "jQuery.noConflict()" and adding that piece of javascript to the pipeline processor for injecting scripts.