Razor: If model is List<> then @Html.LabelFor c

2019-02-24 12:49发布

If the model is a list of objects then @Html.LabelFor model => model[i].member) creates an empty for attribute. The DisplayName works properly, model binding also works just fine. The only thing that does not work is the for attribute.

Below there are two versions of the MVC code. The first 3 code snippets are for the model, controller and view combination that does not work, (the model is a list). The second set of 3 code snippets work. This time the list is wrapped in an object.

What am I missing?

Model:

public class ViewModel {
    public bool ClickMe { get; set; }
}

Controller:

public ActionResult LabelForViewModel() {
    List<ViewModel> model = new List<ViewModel>() {
        new ViewModel() { ClickMe = true}
    };
    return View(model);
}

View:

@model List<ModelBinding.Models.ViewModel>
// form code removed for brevity
@for (var i = 0; i < Model.Count; i++) {
    <div class="form-group">
        @Html.LabelFor(model => model[i].ClickMe)
        <div class="col-md-10">
            @Html.EditorFor(model => model[i].ClickMe, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model[i].ClickMe, "", new { @class = "text-danger" })
        </div>
    </div>
}

Markup:

<div class="form-group">
    <label for="">ClickMe</label>
    <div class="col-md-10">
        <input name="[0].ClickMe" class="form-control check-box" type="checkbox" checked="checked" value="true" data-val-required="The ClickMe field is required." data-val="true"><input name="[0].ClickMe" type="hidden" value="false">
        <span class="field-validation-valid text-danger" data-valmsg-replace="true" data-valmsg-for="[0].ClickMe"></span>
    </div>
</div>

Please notice that the for field is empty and the input has no id field (that one is used in the "for" field).

I have created a wrapper model that contains a list of objects, modified the controller and view and then it works just fine. In this case the "for" field is filled in correctly and everything works as expected.

public class ListWrapper {
    public List<ViewModel> ViewModels { get; set; }
}

View:

@for (var i = 0; i < Model.ViewModels.Count; i++) {
    <div class="form-group">
        @Html.LabelFor(model => model.ViewModels[i].ClickMe)
        <div class="col-md-10">
            @Html.EditorFor(model => model.ViewModels[i].ClickMe, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.ViewModels[i].ClickMe, "", new { @class = "text-danger" })
        </div>
    </div>
}

Markup:

<div class="form-group">
    <label for="ViewModels_0__ClickMe">ClickMe</label>
    <div class="col-md-10">
        <input name="ViewModels[0].ClickMe" class="form-control check-box" id="ViewModels_0__ClickMe" type="checkbox" checked="checked" value="true" data-val-required="The ClickMe field is required." data-val="true"><input name="ViewModels[0].ClickMe" type="hidden" value="false">
        <span class="field-validation-valid text-danger" data-valmsg-replace="true" data-valmsg-for="ViewModels[0].ClickMe"></span>
    </div>
</div>

So, it seems that the for attribute is empty only if the model is a list of objects.

Is this a known issue? Or maybe I'm missing something?

2条回答
时光不老,我们不散
2楼-- · 2019-02-24 12:53

The @model declaration assigns the model object to the Model variable (note the captial 'M'). But then you are reading from model, which hasn't been assigned.

Try this:

  @model List<ModelBinding.Models.ViewModel>
  // form code removed for brevity
        @for (var i = 0; i < Model.Count; i++) {
            <div class="form-group">
                @Html.LabelFor(model => Model[i].ClickMe)
                <div class="col-md-10">
                    @Html.EditorFor(model => Model[i].ClickMe, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessageFor(model => Model[i].ClickMe, "", new { @class = "text-danger" })
                </div>
            </div>
        }

That doesn't explain why your second example works at all, but hey, give this a try.

查看更多
干净又极端
3楼-- · 2019-02-24 13:16

This behavior is the result of the framework complying with HTML-4 specifications. In HTML-4

ID and NAME tokens must begin with a letter ([A-Za-z]) and may be followed by any number of letters, digits ([0-9]), hyphens ("-"), underscores ("_"), colons (":"), and periods (".").

When your model is a collection and you use @Html.EditorFor(m => m[i].SomeProperty) to generate form controls, the method generates name and id attributes based on the name of the property. In this case the name attribute for the first item in the collection will be

name="[0].SomeProperty"

but to avoid clashes with jQuery selectors, the method replaces ., [, and ] characters with an underscore _ when generating the id attribute which would result in

id="_0__SomeProperty"

but because this is invalid according to the HTML-4 specification, the helper does not output it in the html.

The same applies when using @Html.LabelFor() (since the associated form control will not have an id attribute, the value of the for attribute is not output in the html.

Note that in HTML-5, the id attribute would be valid, so hopefully this behavior will be dropped in the future.

Your second example works because id="ViewModels_0__ClickMe" begins with a letter and is therefore valid.

Side note: You could solve the first issue by generating your own id attributes, for example

@Html.LabelFor(model => model[i].ClickMe, new { for = string.Format("item{0}", i) })
@Html.EditorFor(model => model.ViewModels[i].ClickMe, new { htmlAttributes = new { @class = "form-control", id = string.Format("item{0}", i) } })
查看更多
登录 后发表回答