4

I'm building a simple ASP.NET MVC 5.2.3.0 view that displays a user name and a checkbox list of roles. You can check or uncheck the checkboxes to control which roles the user is in. Here is my ViewModel:

public class EditUserVM
{
    public string Id { get; set; }
    public string UserName { get; set; }
    public List<UserRoleVM> Roles { get; set; }
}

public class UserRoleVM
{
    public string RoleId { get; set; }
    public string RoleName { get; set; }
    public bool IsMember { get; set; }
}

And it's displayed like this:

@model  VMContainer<EditUserVM>

@* omitted for exmaple *@

var userVM = Model.ViewModel;
<div class="panel panel-primary">
    <div class="panel-heading"><h4 style="display:inline">User: @userVM.UserName</h4></div>

    <div class="panel-body">
        @Html.ValidationSummary(false, "", new { @class = "text-danger" })

        <h4>Users Roles</h4>

        @using (Html.BeginForm())
        {
            @Html.AntiForgeryToken()

            <div class="form-horizontal">
                @Html.HiddenFor(m => userVM.Id)
                @Html.HiddenFor(m => userVM.UserName)

                <table class="table table-bordered table-striped">
                    <thead><tr><th></th><th>Role</th></tr></thead>
                    <tbody>
                        @for (int x = 0; x<userVM.Roles.Count; x++){
                            <tr>
                                <td>
                                    @Html.HiddenFor(m => userVM.Roles[x].RoleId)
                                    @Html.CheckBoxFor(m => userVM.Roles[x].IsMember)
                                </td>
                                <td>@Html.DisplayFor(m => userVM.Roles[x].RoleName)</td>
                            </tr>
                        }
                    </tbody>
                </table>

                <div>
                    <input type="submit" value="Save" class="btn btn-success" />
                    @Html.ActionLink("Cancel", "Users", null, new { @class = "btn btn-default" })
                </div>
            </div>
        }

Notice the model type is VMContainer. This is a wrapper used by my views so that in case there is an error generating the inner T ViewModel, it can display an error instead of the normal view content. To make it easier to access the EditUserVM and because that is all I need to submit, not the VMContainer wrapper, I assign it to a local variable var userVM = Model.ViewModel;. I've used this pattern a lot in previous projects. The problem is when I inspect the page in developer tools, all of my inputs are named wrong. They all get a prefix for what looks like a generated class. For example, @Html.HiddenFor(m => userVM.Id) renders:

Through trial and error, I narrowed this down to not happening if I remove the for loop. That is with the for loop commented out, the remaining inputs are named correctly. Through further trial and error I discovered that what really triggers it is accessing the Roles indexer using a variable. E.G. If I remove the loop and just do this, it produces the wrong name for all of the inputs:

                @Html.HiddenFor(m => userVM.Id)
                @Html.HiddenFor(m => userVM.UserName)
                @{ int x = 0; }
                @Html.HiddenFor(m => userVM.Roles[x].RoleId);

It does not generate the wrong names if I use the constant [0] in place of [x] or m.ViewModel.Roles[x] instead of userVM.Roles[x]. So to try to spell it out completely, it seems to occur when accessing the indexer of a local variable's descendant using a variable for the index parameter.

I found this or something similar acknowledged for MVC 6 (at the time) but I don't understand the problem. I'm a little surprised this still exists if it is in fact a bug. Although it's hard to describe, it doesn't seem like it would be a totally uncommon scenario. Can someone explain what is going on and hopefully offer a fix? I prefer to only post an EditUserVM to my action when the form submits.

xr280xr
  • 12,621
  • 7
  • 81
  • 125
  • Does `@Html.NameFor(m => userVM.UserName)` show the same result? – jamesSampica Sep 29 '17 at 21:09
  • 1
    Yes, when one of the cases to reproduce listed above is true. I found that if I change `var userVM` to `EditUserVM userVM` that the inputs outside of the for loop get the correct name, but those inside the for loop still get the wrong name. – xr280xr Sep 29 '17 at 21:14
  • Which compiler version & target framework you're using? I think this problem related with CCI (Common Compiler Infrastructure) which adding `CS$<>8__locals` for hidden fields generated with `userVM.Roles` instance, and number suffix "1" belongs to closure classes' count. However I'm still not sure if the CCI directly affects name generation of the hidden fields. – Tetsuya Yamamoto Oct 04 '17 at 01:33
  • @TetsuyaYamamoto I'm using .NET Framework 4.6.2 with Visual Studio 2017 and this is happening when running out of the debugger. Is there a way for me to find the compiler version? – xr280xr Oct 04 '17 at 22:20

1 Answers1

0
@for (int x = 0; x<userVM.Roles.Count; x++){

A workaround could be declaring the variable x in the top of your page.

Source: https://github.com/aspnet/Mvc/issues/2890

HerbalMart
  • 1,669
  • 3
  • 27
  • 50