Handling Checkbox Groups in MVC4
I recently had some work that required a series of checkboxes to be displayed in a view that relate to permissions for a user. The use case was that a user could be a member of one or many groups and those groups determined the level of access within the system. The view needs to render a table similar to the one below.
To make this happen, I have a ViewModelRole object which stores all roles from the database. These roles are stored with the following fields:
- Id - primary key
- Name - description
- IsActive - indicator that shows/hides the record from the system
This is our platform to build on.
Firstly, I will extend the ViewModelRole viewmodel so that it has a IsSelected field. This field will be used to populate a boolean flag that indicates whether the user is a member of that group or not. I did this via a partial class as below
public partial class ViewModelRole
{
public bool IsSelected { get; set; }
}
This gives me an additional field in the UI for each record being retrieved from the database.
The ViewModelRole object is a field of the ViewModelUser object. So that means a one-to-many relationship between User -> Roles. Here is the partial class for ViewModelUser.
public partial class ViewModelUser
{
public List<ViewModelRole> Role { get; set; }
}
In my controller that populates the model for the view, I make a call to my builder which gets all roles from the database, and joins (kind of) that list with a list of roles that the user is already a member of. This populates the User.Roles list that I can use in my view. This is what the builder class looks like.
public List<ViewModelRole> GetUserRoles(int userId)
{
List<ViewModelRole> roles = GetAllRoles();
ViewModelWCUser user = GetUserWithRoles(userId);
List<ViewModelRole> userRoles = user.Role;
foreach (ViewModelRole role in roles)
{
var isSelected = (from r in userRoles
where r.Id == role.Id
select r.Id).FirstOrDefault();
role.IsSelected = isSelected > 0 ? true : false;
}
return roles;
}
You can see in line 12 that the IsSelected field is being populated with true or false based on whether the user is a member of the group or not. Quite simple, right?
So now we have to grab that model data and render it in the view. Here is the Bootstrap table I used to show the data.
<div class="col-lg-12">
<table class="table table-bordered">
<thead>
<tr>
<th></th>
<th>Role</th>
</tr>
</thead>
<tbody>
@{
foreach (var role in Model.User.Role)
{
<tr>
<td>
<input name="userRoles" type="checkbox" checked="@role.IsSelected" value="@role.Id"/>
</td>
<td>@role.Name</td>
</tr>
}
}
</tbody>
</table>
</div>
So far, we have a bootstrap table that shows all roles in the system (that are active) and a checkbox to indicate whether the user is a member of that role. Now we want to wire up a submit so that more or less groups can be configured.
All we need to do is add argument (typed as an array of integers) to the action that accepts the selected Roles. One caveat here is that the argument must have the same name (case sensitive) as the name attribute of the HTML element in the HTML table. Here is the action I used.
// POST: /Admin/User/Edit/5
[HttpPost]
public int Edit(ViewModelUser viewModelUser, int id, int[] userRoles)
{
if (ModelState.IsValid)
{
// update the user. The db call will return the userId
// if the update was successful. Otherwise, 0 is returned
int userId = builder.UpdateUser(viewModelUser.User);
// did the update work?
if (userId == id)
{
// make a list of the selected Roles from the view
List<int> rolesToAddUserTo = new List<int>(userRoles);
// get all the roles from the database
List<ViewModelRole> allRoles = builder.GetAllRoles();
foreach (ViewModelRole role in allRoles)
{
// loop through all roles and "join" on the <em>GetAllRoles</em> list
bool isInList = rolesToAddUserTo.IndexOf(role.Id) != -1;
if (isInList == true)
{
builder.AddRoleToUser(id, role.Id);
}
else
{
builder.RemoveRoleFromUser(id, role.Id);
}
}
}
return userId;
}
return 0;
}
I hope that makes sense to you. Feel free to add some comments or ask questions if any of that doesn't seem right.
Til next time...