0

I'm trying to update my collection of users' roles in my ASP.NET Identity project, but I'm currently stuck because I'm getting a null UsersAndRolesDictionary property in the ViewModel sent to my [HttpPost] method.

Here is my ViewModel, UpdateUserRolesViewModel:

namespace Project_Name.Models
{
    public class UpdateUserRolesViewModel
    {
        public IDictionary<ApplicationUser, ICollection<IdentityUserRole>> UsersAndRolesDictionary { get; set; } // <-- This is returning null currently
    }
}

Here's my HomeController's methods:

[Authorize(Roles = "Admin")]
public ActionResult RoleManager()
{
    ViewBag.Message = "Role Management Page";

    var databaseContext = new ApplicationDbContext();           // Get the Database Context
    var users = databaseContext.Users.Include(u => u.Roles);    // Get all users from the Database and their Roles

    var newDict = new Dictionary<ApplicationUser, ICollection<IdentityUserRole>>();

    // Add each user and their roles to the dictionary
    foreach (var user in users)
    {
        newDict.Add(user, user.Roles);
    }

    // Update the ViewModel with the collection of users and roles
    var updateUserRolesViewModel = new UpdateUserRolesViewModel {UsersAndRolesDictionary = newDict};

    return View(updateUserRolesViewModel);
}

[HttpPost]
[Authorize(Roles = "Admin")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> UpdateUsersRolesAsync(UpdateUserRolesViewModel updateUserRolesViewModel)
{
    try
    {
        //TODO: Attempt to update the user roles or delete the user
        return View("RoleManager");
    }
    catch
    {
        //TODO: Properly catch errors
        return View("RoleManager");
    }
}

Here is my View, RoleManager:

@using Project_Name.Models

@model UpdateUserRolesViewModel

@{
    ViewBag.Title = "Role Manager";
    var databaseContext = new ApplicationDbContext();   // Get the Database Context
    var roles = databaseContext.Roles;                  // Get all Roles from the database, use this to compare against
}

<h2>@ViewBag.Title</h2>

<div class="row">
    <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
        @using (Html.BeginForm("UpdateUsersRolesAsync", "Home", FormMethod.Post))
        {
            @Html.AntiForgeryToken()

            <div class="form-group">
                <div class="table-responsive">
                    <table class="table table-striped table-bordered table-hover">
                        <thead>
                            <tr>
                                <th>Email</th>
                                <th>Roles</th>
                                <th>Delete User?</th>
                            </tr>
                        </thead>
                        <tbody>
                            @{
                                int i = 0; // Used to make unique IDs for the user's table row, and deleteUserCheckbox
                                int j = 0; // Used to make unique IDs for the role checkboxes
                                foreach (var user in Model.UsersAndRolesDictionary.Keys)
                                {
                                    i++;
                                    <tr id="userTableRow_@i">
                                        <td>@user.Email</td>
                                        <td>
                                            @* Show each role availabe as a checkbox. Check them if the user has that role. *@
                                            @foreach (var role in roles)
                                            {
                                                @Html.CheckBox("userRoleCheckbox_" + j++, user.Roles.Any(identityUserRole => identityUserRole.RoleId.Contains(role.Id)))
                                                <span>@role.Name</span>
                                                <br />
                                            }
                                        </td>
                                        <td>
                                            @Html.CheckBox("deleteUserCheckbox_" + i)
                                            <span>Delete User</span>
                                        </td>
                                    </tr>
                                }
                            }
                        </tbody>
                    </table>
                </div>

                @* Reset and Submit buttons *@
                <div class="col-lg-2 col-lg-push-8 col-md-2 col-md-push-8 col-sm-2 col-sm-push-8 col-xs-2 col-xs-push-8">
                    <input type="reset" class="btn btn-danger btn-block" value="Reset" />
                </div>
                <div class="col-lg-2 col-lg-push-8 col-md-2 col-md-push-8 col-sm-2 col-sm-push-8 col-xs-2 col-xs-push-8">
                    <input type="submit" class="btn btn-primary btn-block" value="Submit" />
                </div>

            </div>
        }
    </div>
</div>

I'm using the dictionary UsersAndRolesDictionary to collect all the users and their roles, then enumerating through that to produce my view in the form of a table.

I'm hoping to change the checkbox values of potential multiple users, then passing that updated ViewModel to my [HttpPost] UpdateUsersRolesAsync method in order to update my user roles, but right now I'm getting a null value for the UsersAndRolesDictionary property and I'm not sure why or how to fix it.


Thanks to Stephen Muecke's links/answers in the comments I was able to answer this question. See my answer post below.
Ryan Taite
  • 789
  • 12
  • 37
  • 1
    Is the `updateUserRolesViewModel` instance `null`, or is the `UsersAndRolesDictionary` property of the instance `null`? I have a feeling it's the latter. – JuanR Nov 10 '17 at 19:44
  • 1
    I think your action method should be looking for a `ICollection` instead of the `UpdateUserRolesViewModel `. I'm not sure how well the model binder will bind to a dictionary like you are trying to do. Also, the model binder will only look for things you output as sometype of form field. So, unless you put the user's email in a hidden input, it will be null on the server side. – Tommy Nov 10 '17 at 19:52
  • 2
    @Tommy: That's why I asked about the property. The binder only works with simple types. – JuanR Nov 10 '17 at 19:56
  • @Juan You are correct. My `UsersAndRolesDictionary` property is null, not the ViewModel itself. I'll update my post to reflect that. – Ryan Taite Nov 10 '17 at 20:20
  • You cannot bind to a dictionary containing complex objects like that - your creating `name` attributes for your for controls that have no relationship at all to your model. –  Nov 10 '17 at 20:25
  • 2
    Create a view model representing your data and generate your form controls correctly using a `for` loop or `EditorTemplate` - refer [this answer](https://stackoverflow.com/questions/41258733/bind-dictionary-with-list-in-viewmodel-to-checkboxes/41560929#41560929) for an example –  Nov 10 '17 at 20:29
  • 2
    Refer also [this answer](https://stackoverflow.com/questions/29542107/pass-list-of-checkboxes-into-view-and-pull-out-ienumerable/29554416#29554416) for a typical example of editing user roles –  Nov 10 '17 at 20:31
  • Both of those links do help me better understand the model binding process, thank you! I'm working on altering my code now. – Ryan Taite Nov 10 '17 at 21:07
  • 1
    Possible duplicate of [How does MVC 4 List Model Binding work?](https://stackoverflow.com/questions/14822615/how-does-mvc-4-list-model-binding-work) – JuanR Nov 10 '17 at 21:35
  • Answers go in the answer section - not your question :) –  Nov 11 '17 at 06:49

1 Answers1

0

Following the suggestions of Stephen Muecke in the comments, I have gotten a valid ViewModel to be returned.

Added/updated three ViewModels that combine together:

The first being RoleViewModel:

public class RoleViewModel
{
    public string Id { get; set; }
    public string Name { get; set; }
    public bool IsSelected { get; set; }
}

Second being UserViewModel:

public class UserViewModel
{
    public string Id { get; set; }
    public string Email { get; set; }
    public List<RoleViewModel> RoleViewModels { get; set; }
    public bool DeleteUser { get; set; } // Doesn't work yet, might be in the wrong place
}

And finally the third being an updated version of UpdateUserRoleViewModel:

public class UpdateUserRolesViewModel
{
    public int Id { get; set; }
    public List<UserViewModel> UserViewModels { get; set; }
}

In my updated HomeController are the methods again:

[Authorize(Roles = "Admin")]
public ActionResult RoleManager()
{
    ViewBag.Message = "Role Management Page";

    var databaseContext = new ApplicationDbContext();                   // Get the Database Context
    var users = databaseContext.Users.Include(u => u.Roles).ToList();   // Get all users from the Database and their Roles

    // Create the UpdateUserRolesViewModel
    var updateUserRolesViewModel = new UpdateUserRolesViewModel
    {
        Id = 0, // Not sure what else the Id would be
        UserViewModels = new List<UserViewModel>()
    };

    // Add each user to the UserViewModels list
    for (int i = 0; i < users.Count(); i++)
    {
        var userViewModel = new UserViewModel
        {
            Id = users.AsEnumerable().ElementAt(i).Id,
            Email = users.AsEnumerable().ElementAt(i).UserName,
            RoleViewModels = new List<RoleViewModel>(),
            DeleteUser = false
        };

        // Add each role from the Roles table to the RoleViewModels list, check if user has that role
        foreach (var role in databaseContext.Roles)
        {
            var roleViewModel = new RoleViewModel
            {
                Id = role.Id,
                Name = role.Name,
                IsSelected = users.AsEnumerable().ElementAt(i).Roles.Any(identityUserRole => identityUserRole.RoleId.Contains(role.Id))
            };

            userViewModel.RoleViewModels.Add(roleViewModel);
        }

        updateUserRolesViewModel.UserViewModels.Add(userViewModel);
    }

    return View(updateUserRolesViewModel);
}

[HttpPost]
[Authorize(Roles = "Admin")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> UpdateUsersRolesAsync(UpdateUserRolesViewModel updateUserRolesViewModel)
{
    try
    {
        // Attempt to update the user roles
        foreach (var user in updateUserRolesViewModel.UserViewModels)
        {
            // Delete user
            //TODO: Prompt user to confirm deletion if one or more people are being deleted
            if (user.DeleteUser)
            {
                var userToDelete = await UserManager.FindByIdAsync(user.Id);    // Get the ApplicationUser object of who we want to delete
                await UserManager.DeleteAsync(userToDelete);                    // Delete the user
                continue;                                                       // Don't try to update the roles of a deleted user.
            }

            // Remove all roles from the User
            var rolesToRemove = await UserManager.GetRolesAsync(user.Id);
            await UserManager.RemoveFromRolesAsync(user.Id, rolesToRemove.ToArray());

            // Add roles to the User
            foreach (var roleViewModel in user.RoleViewModels.Where(r => r.IsSelected))
            {
                await UserManager.AddToRoleAsync(user.Id, roleViewModel.Name);
            }
        }

        return RedirectToAction("RoleManager");
    }
    catch
    {
        //TODO: Properly catch errors
        return RedirectToAction("RoleManager");
    }
}

Finally, here is my View, RoleManager

@using Project_Name.ViewModels

@model UpdateUserRolesViewModel

@{
    ViewBag.Title = "Role Manager";
}

@* Debugging text *@
@foreach (var user in Model.UserViewModels)
{
    <div>User ID: @user.Id</div>
    <div>User Name: @user.Email</div>
    <p>
        @foreach (var roleViewModel in user.RoleViewModels.Where(r => r.IsSelected))
        {
            <div>Role ID: @roleViewModel.Id</div>
            <div>Role Name: @roleViewModel.Name</div>
        }
    </p>
    <hr />
}

<h2>@ViewBag.Title</h2>

<div class="row">
    <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
        @using (Html.BeginForm("UpdateUsersRolesAsync", "Home", FormMethod.Post))
        {
            @Html.AntiForgeryToken()
            @Html.HiddenFor(m => m.Id)
            <div class="form-group">
                <div class="table-responsive">
                    <table class="table table-striped table-bordered table-hover">
                        <thead>
                            <tr>
                                <th>Email</th>
                                <th>Roles</th>
                                <th>Delete User?</th>
                            </tr>
                        </thead>
                        <tbody>
                            @for (int i = 0; i < Model.UserViewModels.Count; i++)
                            {
                                <tr id="userTableRow_@i">
                                    <td>
                                        @Html.HiddenFor(m => m.UserViewModels[i].Id)
                                        @Html.HiddenFor(m => m.UserViewModels[i].Email)
                                        @Model.UserViewModels[i].Email
                                    </td>
                                    <td>
                                        @for (int j = 0; j < Model.UserViewModels[i].RoleViewModels.Count; j++)
                                        {
                                            @Html.HiddenFor(m => m.UserViewModels[i].RoleViewModels[j].Id)
                                            @Html.HiddenFor(m => m.UserViewModels[i].RoleViewModels[j].Name)
                                            @Html.CheckBoxFor(m => m.UserViewModels[i].RoleViewModels[j].IsSelected)
                                            @Html.DisplayTextFor(m => m.UserViewModels[i].RoleViewModels[j].Name)
                                            <br/>
                                        }
                                    </td>
                                    <td>
                                        @Html.CheckBoxFor(m => m.UserViewModels[i].DeleteUser)
                                        @Html.DisplayNameFor(m => m.UserViewModels[i].DeleteUser)
                                    </td>
                                </tr>
                            }
                        </tbody>
                    </table>
                </div>

                @* Reset and Submit buttons *@
                <div class="col-lg-2 col-lg-push-8 col-md-2 col-md-push-8 col-sm-2 col-sm-push-8 col-xs-2 col-xs-push-8">
                    <input type="reset" class="btn btn-danger btn-block" value="Reset" />
                </div>
                <div class="col-lg-2 col-lg-push-8 col-md-2 col-md-push-8 col-sm-2 col-sm-push-8 col-xs-2 col-xs-push-8">
                    <input type="submit" class="btn btn-primary btn-block" value="Submit" />
                </div>

            </div>
        }
    </div>
</div>

This now updates the user's Roles, and Deletes them (though there is no confirmation check so be careful with that!)

Ryan Taite
  • 789
  • 12
  • 37