Adding html spans into a LabelFor - MVC 3

2019-04-26 12:33发布

问题:

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!

回答1:

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.



回答2:

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).