Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Adding html spans into a LabelFor - MVC 3

I've got a complicated form, which needs to be used by both javascript and non-javascript users. My application is .NET MVC 3.

The form has plenty of fields, which have to be filled in if they answer a particular way to a previous question. With javascript enabled, I'm able to show or hide the relevant question based on a user interaction. With no javascript I would like to add more detail to the question label itself because by default all of the questions are shown (much like filling in a paper version of the form).

Ideally I would like the label to be something like:

<label><span class="non-js">If you were a smoker, when</span><span class="js">When</span> did you stop smoking?</label>

This way I can switch off the relevant text in CSS (I'm hoping a screenreader can cope with this).

So javascript users get:

When did you stop smoking?

and non-javascript users get:

If you were a smoker, when did you stop smoking?

My question is how would I go about doing this, as LabelFor helpers don't allow html in the string.

Update

Sorry, I forgot one crucial bit of info, in that my label is currently being populated by a [Display(Name = "When did you stop smoking?")] annotation in the model. Idelly I would like to keep this here and have something like:

[Display(JS = "When", NonJS = "If you were a smoker, when", Both="did you stop smoking?")]

Is this possible?

Update 2

OK here's what I've got so far. This is my attribute:

public class MultipleDisplayNameAttribute : Attribute, IMetadataAware
{
    public string CustomerDisplayName { get; set; }
    public string CustomerJSAlternative { get; set; }
    public string CustomerNonJSAlternative { get; set; }

    public void OnMetadataCreated(ModelMetadata metadata)
    {
        string jsPart;
        string nonJsPart;
        jsPart = (CustomerJSAlternative != null) ? String.Format("<span class='spnJs'>{0}</span>", CustomerJSAlternative) : "";
        nonJsPart = (CustomerNonJSAlternative != null) ? String.Format("<span class='spnNonJs'>{0}</span>", CustomerNonJSAlternative) : "";
        metadata.DisplayName = String.Format("{0}{1}{2}", jsPart, nonJsPart, CustomerDisplayName);
    }
}

Unfortunately I'm now stuck as this is displayed unencoded on the screen. i.e. it is actually coming out like this on screen:

<span class="non-js">If you were a smoker, when</span><span class="js">When</span> did you stop smoking?

Is there any way to change the displayName metadata property to cope with this?

Update 3 and solution

With the help of the answers and http://weblogs.asp.net/imranbaloch/archive/2010/07/03/asp-net-mvc-labelfor-helper-with-htmlattributes.aspx I managed to get the solutions I was after:

My attribute:

public class MultipleDisplayNameAttribute : Attribute, IMetadataAware
{
    public string CustomerDisplayName { get; set; }
    public string CustomerJSAlternative { get; set; }
    public string CustomerNonJSAlternative { get; set; }

    public void OnMetadataCreated(ModelMetadata metadata)
    {
        metadata.AdditionalValues["JS"] = (CustomerJSAlternative != null) ? String.Format("<span class='spnJs'>{0}</span>", CustomerJSAlternative) : "";
        metadata.AdditionalValues["NoJS"] = (CustomerNonJSAlternative != null) ? String.Format("<span class='spnNonJs'>{0}</span>", CustomerNonJSAlternative) : "";
        metadata.DisplayName = CustomerDisplayName;
    }
}

My helper:

    public static MvcHtmlString JsAndNonJsCheckFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
    {
        var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
        HtmlString jsLabel = new HtmlString("");
        HtmlString noJsLabel = new HtmlString("");
        string htmlFieldName = ExpressionHelper.GetExpressionText(expression);

        string labelText = metadata.DisplayName ?? metadata.PropertyName ?? htmlFieldName.Split('.').Last();
        if (String.IsNullOrEmpty(labelText))
            return MvcHtmlString.Empty;


        if(metadata.AdditionalValues.ContainsKey("JS"))
            jsLabel = new HtmlString((string)metadata.AdditionalValues["JS"]);


        if (metadata.AdditionalValues.ContainsKey("NoJS"))
            noJsLabel = new HtmlString((string)metadata.AdditionalValues["NoJS"]);

        TagBuilder tagBuilder = new TagBuilder("label");
        tagBuilder.Attributes.Add("for", html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(htmlFieldName));
        tagBuilder.InnerHtml = String.Format("{0}{1}{2}", jsLabel, noJsLabel, labelText);

        return MvcHtmlString.Create(tagBuilder.ToString(TagRenderMode.Normal));
    }

Job done (eventually!) Thanks for everyone's help!

like image 989
Sniffer Avatar asked Oct 19 '11 15:10

Sniffer


2 Answers

Your current problem is just how LabelFor works. This is the decompiled source code for LabelFor (from MVC2. The actual source is probably nicer, and is freely available for download):

    public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
    {
      return LabelExtensions.LabelHelper((HtmlHelper) html, ModelMetadata.FromLambdaExpression<TModel, TValue>(expression, html.ViewData), ExpressionHelper.GetExpressionText((LambdaExpression) expression));
    }

    internal static MvcHtmlString LabelHelper(HtmlHelper html, ModelMetadata metadata, string htmlFieldName)
    {
      string str = metadata.DisplayName;
      if (str == null)
      {
        string propertyName = metadata.PropertyName;
        if (propertyName == null)
          str = Enumerable.Last<string>((IEnumerable<string>) htmlFieldName.Split(new char[1]
          {
            '.'
          }));
        else
          str = propertyName;
      }
      string innerText = str;
      if (string.IsNullOrEmpty(innerText))
        return MvcHtmlString.Empty;
      TagBuilder tagBuilder = new TagBuilder("label");
      tagBuilder.Attributes.Add("for", html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(htmlFieldName));
      tagBuilder.SetInnerText(innerText);
      return tagBuilder.ToMvcHtmlString(TagRenderMode.Normal);
    }
  }

You could basically copy this code, change the method name, and change the second last line of code to be:

tagBuilder.InnerHtml(innerText);

And it you will have a working solution which doesn't escape your HTML.

like image 88
Kevin Stricker Avatar answered Nov 12 '22 08:11

Kevin Stricker


You can write your own LabelFor extension to Html. I would recommend this since you could pass in the js and non-js text as parameters instead of having to write the HTML over and over again. Here's the basic Label type helper:

using System;
namespace MvcApplication1.Helpers
{
      public class LabelHelper
      {
           public static string LabelJsNonJs(this HtmlHelper helper, string target, string nonJs, string js, string commonText)
           {
                return String.Format("<label for='{0}'><span class='non-js'>{1}</span><span class='js'>{2}</span>{3}</label>", target, nonJs, js, commonText);
           }
      }
}

You can use this in your views by adding a using for your MvcApplication1.Helpers namespace.

See http://weblogs.asp.net/imranbaloch/archive/2010/07/03/asp-net-mvc-labelfor-helper-with-htmlattributes.aspx for more info on making a LabelFor type helper (a little more complicated and I don't have much time).

like image 36
Milimetric Avatar answered Nov 12 '22 09:11

Milimetric