3

I am having issues posting a viewmodel which contains a dictionary. Each time I receive the model at my controller action, all values are null (really only the dictionary values that I need) and I'm not sure how to resolve this or why it's happening. Below you can find my view (nonrelevant parts excluded), my controller, and the viewmodel I am using. The idea here is to pass a dictionary relating users to roles, allow an admin to edit the various user roles, then pass the information back to the controller to update the DB with the new role info. I have had luck serializing IEnumerables in the past (Lists, etc) this way but cant get it to work for a dictionary. All help is appreciated.

The View:

@model RecipeManager.ViewModels.AdminViewModel

@{
    ViewBag.Title = "Index";
}
<h2>Admin</h2>
@using (Html.BeginForm("UpdateRoles", "Admin", FormMethod.Post))
{
    <div>
        @for (int i = 0; i < Model.UsersAndRoles.Count; i++)
        {
            <strong>@Model.UsersAndRoles.ElementAt(i).Key.UserName</strong> 
            @Html.DropDownListFor(m => m.UsersAndRoles.ElementAt(i).Value, Model.RoleTypes, Model.UsersAndRoles.ElementAt(i).Value, new { @class = "form-control" })
        }
    </div>
    <input type="submit" value="Update Roles" />
}

An example of the html produced a username and the admin role dropdown:

<strong>test+cook@test.com</strong> <select class="form-control" id="Value" name="Value"><option value="">Cook</option>
<option value="1">Admin</option>
<option value="2">Cook</option>
<option value="3">Retail</option>
</select>   

The Controller Action (in AdminController):

[AuthorizeUser(Activity = "ViewAdmin")]
[HttpPost]
public ActionResult UpdateRoles(AdminViewModel vm)
{
  return View("Index");
}

And the ViewModel (AdminViewModel):

namespace RecipeManager.ViewModels
{
  public class AdminViewModel
  {

    public SelectList RoleTypes { get; set; }

    public String SelectedRole { get; set; }

    public String InviteEmailAddress { get; set; }

    public Dictionary<ApplicationUser, string> UsersAndRoles { get; set; }


  }
}
Saadi
  • 2,079
  • 2
  • 16
  • 45
GregH
  • 4,909
  • 3
  • 36
  • 90
  • Could you show the HTML that is emitted by that razor code? – Crowcoder Jun 04 '16 at 13:23
  • You are passing in serialized object? – Karolis Kajenas Jun 04 '16 at 13:23
  • Possible duplicate of [Correct, idiomatic way to use custom editor templates with IEnumerable models in ASP.NET MVC](http://stackoverflow.com/questions/25333332/correct-idiomatic-way-to-use-custom-editor-templates-with-ienumerable-models-in) – GSerg Jun 04 '16 at 13:26
  • @Crowcoder I have posted an example of the html produced (atleast the parts relating to the dictionary) – GregH Jun 04 '16 at 13:33
  • 1
    @peggy, that post data cannot be understood by the model binder. Did you look at the link provided by GSerg? – Crowcoder Jun 04 '16 at 14:56

1 Answers1

2

You generating a <select> element with a name attribute (name="Value") that has no relationship with your property which is a Dictionary. To bind to a Dictionary to need inputs for both the Key and Value properties, and because its a collection, they needs to be indexed as well. In your case it gets even worse because your Key is a complex object.

In order to bind, your generated html would need to be

<input type="hidden" name="UsersAndRoles[0].Key.Id" value="...." />
<input type="hidden" name="UsersAndRoles[0].Key.UserName" value="...." />
.... // more hidden inputs for each property of ApplicationUser
<select name="UsersAndRoles[0].Value">
    <option value="">Cook</option>
     ....
</select>

<input type="hidden" name="UsersAndRoles[1].Key.Id" value="...." />
<input type="hidden" name="UsersAndRoles[1].Key.UserName" value="...." />
.... // more hidden inputs for each property of ApplicationUser
<select name="UsersAndRoles[1].Value">
    <option value="">Cook</option>
     ....
</select>

The standard HtmlHelper methods will not do this out of the box (although you could create your own extension method to generate the html). But it will be far easier if you just design your view model to represent what it is that you want to edit in the view.

However its not clear from your current view model what your wanting to bind to. You have a public String SelectedRole { get; set; } property which is not used and Dictionary Value is typeof string but the values of the SelectListItem are typeof int. In addition, using a DropDownListFor() method will not work correctly unless you use an EditorTemplate in conjunction with AdditionalViewData or generate a new IEnumerable<SelectListItem> as described in this answer. (notice that none of the options have the selected attribute even though the 3rd option ("cook") should be). Note also that the 3rd parameter of DropDownListFor() generates the null option so it should be "Please select" or similar, not the value of the current role.

Your view models (based on the view code you have shown) should be

public class AdminViewModel
{
    public SelectList RoleTypes { get; set; }
    public List<UserRoleViewModel> UserRoles { get; set; }
}
public class UserRoleViewModel
{
    public int ID { get; set; }
    public string UserName { get; set; }
    [Display(Name = "Role")]
    [Required(ErrorMesage = "Please select a role")]
    public int SelectedRole { get; set; }
}

Then create an EditorTemplate in /Views/Shared/EditorTemplates/UserRoleViewModel.cshtml

@model UserRoleViewModel
@Html.HiddenFor(m => m.ID)
@Html.HiddenFor(m => m.UserName)
<strong>@Html.DisplayFor(m => m.UserName)</strong>
@Html.LabelFor(m => m.SelectedRole)
@Html.DropDownListFor(m => m.SelectedRole, (SelectList)ViewData["RoleTypes"], "Please Select")
@Html.ValidationMessageFor(m => m.SelectedRole)

and then in the main view

@model RecipeManager.ViewModels.AdminViewModel
....
@using (Html.BeginForm("UpdateRoles", "Admin", FormMethod.Post))
{
    @Html.EditorFor(m => m.UserRoles, new { RoleTypes = Model.RoleTypes })
    <input type="submit" value="Update Roles" />
}
Community
  • 1
  • 1