views:

143

answers:

1

When using the following code, the id's of the field and the id in the for attribute of the label isn't identical.

<%: Html.LabelFor(x => x.Localizations["en"]) %>   => Localizations[en]
<%: Html.TextBoxFor(x=> x.Localizations["en"]) %>  => Localizations_en_

<%: Html.LabelFor(x => x.Localizations["en"].Property) %>
       => Localizations[en]_Property
<%: Html.TextBoxFor(x=> x.Localizations["en"].Property) %>
       => Localizations_en__Property

I traced the code in reflector and saw that the way the values are generated are different. Not using the same helper method.

LabelFor uses HtmlHelper.GenerateIdFromName and TextBoxFor uses TagBuilder#GenerateId.

Does anyone know the reason for this, or a workaround (except writing your own entire set of input/textarea/select helpers)? Or is it a bug?

UPDATE:

Since I anyway use a html helper for the label with a second parameter for the label text, I did modify it to use the same id generation code as the form field helpers.

public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, string labelText)
{
    // The main part of this code is taken from the internal code for Html.LabelFor<TModel, TValue>(...).
    var metaData = ModelMetadata.FromLambdaExpression(expression, helper.ViewData);
    var fieldName = ExpressionHelper.GetExpressionText(expression);

    TagBuilder builder = new TagBuilder("label");
    // Generate the id as for the form fields (adds it to self).
    builder.GenerateId(fieldName);
    // Use the generated id for the 'for' attribute.
    builder.Attributes.Add("for", builder.Attributes["id"]);
    // Remove the id again.
    builder.Attributes.Remove("id");
    builder.SetInnerText(labelText);
    return MvcHtmlString.Create(builder.ToString());
}

This solves my immediate problem, but it doesn't answer the question as to why the implementation looks like it does in MVC2. If there's an reason for it.

By the way: There's no need to actually modify the id/for attribute in HTML5, since it's perfectly legal to have an id looking like ^~[] if you'd like to. All major browsers support it. This is nicely explained by Mathias Bynens.

UPDATE 2:

This does not solve the problem at all actually, since the DefaultModelBinder can't bind to it anyway. Using nested objects in dictionaries doesn't seem to be supported by the field name generator in MVC 2, since it generates:

<input type="text" name="Dict[en]" value="(VALUE)">

Instead of what the model binder wants:

<input type="hidden" name="Dict[0].Key" value="en">
<input type="text" name="Dict[0].Value" value="(VALUE)">

Strange that it comes out of the box this way.

I've tried create a custom model binder for it, but I can't get MVC2 to use it whatever I try to use it on:

ModelBinders.Binders.Add(typeof(IDictionary<string,object>), new DictionaryModelBinder());
ModelBinders.Binders.Add(typeof(IDictionary<string,string>), new DictionaryModelBinder());
ModelBinders.Binders.Add(typeof(IDictionary), new DictionaryModelBinder());
ModelBinders.Binders.Add(typeof(Dictionary), new DictionaryModelBinder());

So right now it looks like it's back to manually creating the name attribute values with hidden .Key fields.

+1  A: 

MVC intentionally mungs ids with special characters which are significant in jQuery/CSS3 selectors. This is because the selector syntax becomes complicated when there are "reserved" (by jQuery/CSS3) characters in the ID.

It does not do this with name because it isn't necessary there and gets in the way of binding.

It's certainly a bug if LabelFor doesn't actually point to the corresponding TextBoxFor. But I'd argue the bug is in LabelFor, not in TextBoxFor.

Craig Stuntz