LibrarySites.Banner

Translating and Editing Form Labels in MVC Solutions Using the Sitecore ASP.NET CMS

This blog post describes an experimental approach to supporting inline editing of HTML form labels in the Page Editor and translation of such labels for MVC solutions using the Sitecore ASP.NET web Content Management System (CMS) and Custom Engagement Platform (CEP).

HTML forms typically provide labels for the various fields used to capture data from site visitors. The System.Web.Mvc.Html.LabelExtensions class that extends the System.Web.Html.HtmlHelper class exposed by the @Html property in MVC views provides the LabelFor() method to generate labels for HTML input elements that correspond to properties of the model passed to the view. The LabelFor() method can use the Display attribute of the property associated with the HTML input element to determine the value for the label of that element.

If your site supports multiple languages, you might want to provide appropriate translations of your labels. For example, you might want to create fields for the labels in the content items that serve as data sources for the form views, or you might want to retrieve translations from the Sitecore dictionary system. To support this functionality, you can override the System.ComponentModel.DisplayNameAttribute class with your own implementation such as the following:

namespace Sitecore.Sharedsource.Mvc.ComponentModel
{
  using System.ComponentModel;
  using System.Web.Mvc;
  
  using SC = Sitecore;
  
    public class SitecoreDisplayNameAttribute : DisplayNameAttribute
  {
    // for rendering field values
    private Sitecore.Mvc.Helpers.SitecoreHelper sitecoreHelper;
  
    // the name or ID of the field containing translated label value
    public string Key { get; set; }
  
    // detemrine display name for property identified by Key
    public override string DisplayName
    {
      get
      {
        SC.Diagnostics.Assert.ArgumentNotNullOrEmpty(this.Key, "Key");
  
        // check for a value in the specified field of the rendered item
        // use SitecoreHelper to support inline editing
        string value = this.SitecoreHelper.Field(this.Key).ToString();
  
        if (string.IsNullOrWhiteSpace(value))
        {
          // otherwise check the translation dictionary
          value = SC.Globalization.Translate.Text(this.Key);
        }
  
        return value;
      }
    }
  
    // lazy load sitecoreHelper
    private Sitecore.Mvc.Helpers.SitecoreHelper SitecoreHelper
    {
      get
      {
        if (this.sitecoreHelper == null)
        {
          this.sitecoreHelper = 
            SC.Mvc.Helpers.ThreadHelper.GetThreadData<SC.Mvc.Helpers.SitecoreHelper>();
        }
  
        if (this.sitecoreHelper == null)
        {
          ViewContext viewContext = SC.Mvc.Common.ContextService.Get().GetCurrent<ViewContext>();
          SC.Diagnostics.Assert.IsNotNull(viewContext, "viewContext");
          HtmlHelper htmlHelper = new HtmlHelper(
            viewContext, 
            new SC.Mvc.Presentation.ViewDataContainer(viewContext.ViewData));
          SC.Diagnostics.Assert.IsNotNull(htmlHelper, "htmlHelper");
          this.sitecoreHelper = new SC.Mvc.Helpers.SitecoreHelper(htmlHelper);
          SC.Diagnostics.Assert.IsNotNull(this.sitecoreHelper, "sitecoreHelper");
          SC.Mvc.Helpers.ThreadHelper.SetThreadData<SC.Mvc.Helpers.SitecoreHelper>(
            this.sitecoreHelper);
        }
  
        return this.sitecoreHelper;
      }
    }
  }
}

In your models (intentionally oversimplified for this example), you can use the Key property of the SitecoreDisplayName attribute to indicate the ID or name of the field that contains the value for the label or an entry in the Sitecore dictionary system:

namespace Sitecore.Sharedsource.Mvc.Models.Forms
{
  using System.ComponentModel.DataAnnotations;
  
  public class ContactUsForm
  {
    [Sitecore.Sharedsource.Mvc.ComponentModel.SitecoreDisplayName(Key = "Email")]
    [Required(AllowEmptyStrings = false), StringLength(256)]
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }
  }
}

For convenience, add an extension to HtmlHelper to retrieve the label element:

namespace Sitecore.Sharedsource.Mvc.Helpers
{
  using System;
  using System.Linq.Expressions;
  using System.Web;
  using System.Web.Mvc;
  using System.Web.Mvc.Html;
  
  public static class SitecoreHelperExtensions
  {
    public static string EditableLabelFor<TModel, TValue>(
      this HtmlHelper<TModel> html, 
      Expression<Func<TModel, TValue>> expression, 
      string labelText = null)
    {
      return HttpUtility.HtmlDecode(
        LabelExtensions.LabelFor<TModel, TValue>(html, expression, labelText).ToString());
    }
  }
}

In your views, you must use the Raw() method of the @Html property to prevent ASP.NET from escaping the angle brackets in the HTML hidden input elements that support inline editing:

@model Sitecore.Sharedsource.Mvc.Models.Forms.ContactUsForm
@using (Html.BeginForm())
{   
  @Html.Sitecore().FormHandler()
  @Html.Raw(Html.EditableLabelFor(model => this.Model.Email))
  @Html.TextBoxFor(model => Model.Email)
  <input type="submit" value="Submit" />
}

This solution is completely experimental and may present problems. For example, I did not test conditions such as if the value for the label contains HTML special characters including ampersand (&) and greater-than (<).

Resources:

  • I like the valuable info you provide in your articles. I’ll bookmark your weblog and check again here regularly. I’m quite sure I’ll learn many new stuff right here! Good luck for the next!

  • John - where does the item that contains the field "Email" get placed into the context?  Assuming it is not the PageContext item, how can I tell the view what item to use for the label?  (for example, a search form that appears on all pages)...

  • @Brian: There is an ongoing debate about where to store form labels in Sitecore (with or without MVC, it's the same issue). I see at least three options:  1. Put fields for the labels in the data template for the context item. Convenient and flexible, this keeps all of the data in one place, but may not be appropriate where you share the same labels on multiple forms (and don't want to duplicate those values in multiple items), and leads to additional data templates. Still, it's my preference. Then you just get them from the data source item just like any other field value.  2. Put the fields for the labels in some other item. Then something has to determine where that item is - maybe you hard-code its ID or use some system to determine it dynamically. Then you need to pass that item as an additional argument to the Field() method in SitecoreHelper.  3. You can use the Sitecore dictionary, but then you don't get inline editing for the form labels.  You can take some kind of hybrid approach - for example, first check for the field in the context item, then some specific item for labels for this form, and finally the dictionary. That way you can put labels in specific items and add them to individual items if you find you need to override them for specific pages.  Of course there are additional options such as resource files.  Please let me know if this does not make sense, does not answer the question, leads to more questions, doesn't work, if there is an option I missed, etc.  

  • John - I am sorry, I wasn't clear enough - I have an item with the fields within it...since this is a form that occurs on all pages, it is within the site shared content area.   My question is how does the item with these fields in it get "fed" to the view?  In this case, I have a controller rendering that is responsible for finding the shared content item, but I do not see how the item is sent to the razor view responsible for rendering the form.  In your example, the ContactUsForm is the model - what Item is that model reading from?  How does it know what item to use?   Thanks for the help.

  • Sorry, I am not very familiar with controller renderings. For a view rendering, you would set the data source of the rendering to that item that contains the fields for the labels. I am going to ask someone with more controller experience to take a look at this post and if they don't have time, I might try to test some things.

  • John - playing with this a bit more indicates that the rendering of the field from the annotation does come from the PageContext item - so if you tried to use the same with an item served via a datasource might be problematic....thanks for any help you can offer - I certainly love the idea of using somewhat standard syntax for Sitecore forms...

  • John - I was able to get this to work, although I am not sure exactly how good the implementation is:  (1) make my Form model class inherit from RenderingModel  public class KeywordSearchForm : RenderingModel     {         [SitecoreDisplayName(Key = "Search Field Label")]         [Required(ErrorMessage = "*")]         [StringLength(50)]         public string SearchText { get; set; }      }  (2) in the controller (part of the rendering controller) .. I added the following:  public ActionResult Index()         {                         string previousSearch = (string)Session["site.lastsearch"];              Item contextItem = Sitecore.Context.Item;              SharedContentItem myItem = Services.FindPlatformSettings(contextItem);             RenderingContext.Current.Rendering.Item = myItem.InnerItem;             KeywordSearchForm form = new KeywordSearchForm                 {                                         Rendering = RenderingContext.Current.Rendering,                     SearchText = previousSearch                 };              return PartialView("~/views/partial/Search/_EnterpriseSearchForm.cshtml", form);          }

  • Are there any updates to this 6 year old article? For example, is this functionality now built in to Sitecore 8 or 9? There are third-party add-ons that do this, but is it now native to Sitecore 8 or 9?