I know this might be a lot of code to look at it, but it seemed like it was necessary to share it. Thanks in advance for reading!
I am building an application starting with the ASP.NET MVC 5 default template. I want to add a checkbox list of Identity's ApplicationRoles to the Register action of the Account controller.
So, rather than just collect the first and last names, email, phone number, etc., I also want to supply a checkbox list of roles in the database.
I've added this to the RegisterViewModel (in AccountViewModels.cs):
[Required]
[Display(Name = "Roles List")]
public IEnumerable<SelectListItem> RolesList { get; set; }
I changed the Account controller's HttpGet Register action from this:
// GET: /Account/Register
public ActionResult Register()
{
return View();
}
to this:
// GET: /Account/Register
[HttpGet]
public ActionResult Register()
{
//Populate the roles checkbox list for the view
RegisterViewModel model = new RegisterViewModel
{
RolesList = RoleManager.Roles.OrderBy(r => r.Name).ToList().Select(r => new SelectListItem()
{
Text = r.Name,
Value = r.Name,
Disabled = (r.Name == "Admin" && !User.IsInRole("Admin"))
})
};
return View(model);
}
Finally, I updated the Account controller's HttpPost Register action to this:
// POST: /Account/Register
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model, params string[] rolesSelectedOnView)
{
if (ModelState.IsValid)
{
rolesSelectedOnView = rolesSelectedOnView ?? new string[] { };
var user = new ApplicationUser { FirstName = model.FirstName, LastName = model.LastName, PhoneNumber = model.PhoneNumber, UserName = model.Email, Email = model.Email};
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
var rolesAddResult = await UserManager.AddToRolesAsync(user.Id, rolesSelectedOnView.ToString());
if (!rolesAddResult.Succeeded)
{
ModelState.AddModelError("", rolesAddResult.Errors.First());
AddErrors(rolesAddResult);
return View(model);
}
string callbackUrl = await SendEmailConfirmationTokenAsync(user.Id, "Confirm your account");
ViewBag.Message = "A confirmation email has been sent to the address you specified. Please have "
+ "the person check their email and confirm their account. The account must be confirmed "
+ "from the confirmation email before they can log in.";
return View("Info");
//return RedirectToAction("Index", "Home");
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
The Register view looks (in part) like this:
@model MngiReferrals.Models.RegisterViewModel
@{
ViewBag.Title = "Register";
}
<h2>@ViewBag.Title.</h2>
@using (Html.BeginForm("Register", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
@Html.AntiForgeryToken()
<h4>Create a new account.</h4>
...removed...
<div class="form-group">
@Html.Label("Roles", new { @class = "col-md-offset-2 col-md-10" })
<span class="col-md-offset-2 col-md-10">
@foreach (var item in Model.RolesList)
{
<input type="checkbox" name="RolesList" value="@item.Value" class="checkbox-inline" />
@Html.Label(item.Value, new {@class = "control-label"})
<br />
}
</span>
</div>
This allows the Register view to render with the normal fields and the list of roles in the database. However, when I submit the form, it doesn't try to validate the roles list (even though I've marked it as [Required] in the view model. Furthermore, it returns me to the Register form with the fields filled in, but then the checkbox list of roles is no longer on the form.
Finally, if I try to submit the form again, it returns this error from the view:
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.NullReferenceException: Object reference not set to an instance of an object.
Line 51: @Html.Label("Roles", new { @class = "col-md-offset-2 col-md-10" })
Line 52: <span class="col-md-offset-2 col-md-10">
Line 53: @foreach (var item in Model.RolesList)
Line 54: {
Line 55: <input type="checkbox" name="RolesList" value="@item.Value" class="checkbox-inline" />
After making these changes, the user is no longer registered in the database, so I'm not sure I'm even ever making it to the HttpPost Register action.
I would appreciate it if someone could help me fill in the blanks on this problem. Thank you in advance!
UPDATE #1
I updated my code based on a previous answer by @StephenMuecke (see his comment below for the link). I am close, but it looks like I am not correctly capturing the selected checkbox values.
Here is what this looks like now.
RegisterViewModel (in AccountViewModels.cs):
public class RegisterViewModel
{
[Required]
[Display(Name = "First Name")]
public string FirstName { get; set; }
[Required]
[Display(Name = "Last Name")]
public string LastName { get; set; }
...more properties...
[Required]
[Display(Name = "Roles List")]
public IEnumerable<SelectListItem> RolesList { get; set; }
public RegisterViewModel()
{
RolesList = new List<ApplicationRoleRegisterViewModel>();
}
}
ApplicationRoleRegisterViewModel (new View Model for the ApplicationRoles)
public class ApplicationRoleRegisterViewModel
{
[Required]
public string Name { get; set; }
public bool IsSelected { get; set; }
public bool IsDisabled { get; set; }
}
HttpGet Account Register action:
// GET: /Account/Register
[HttpGet]
public ActionResult Register()
{
//Populate the roles checkbox list for the view
var model = new RegisterViewModel { RolesList = new List<ApplicationRoleRegisterViewModel>() };
var roles = RoleManager.Roles.OrderBy(r => r.Name);
foreach (var role in roles)
{
var roleVm = new ApplicationRoleRegisterViewModel
{
Name = role.Name,
IsSelected = false, // Since this is for a user that does not yet exist, this would initially be deselected.
IsDisabled = role.Name == "Admin" && !User.IsInRole("Admin")
};
model.RolesList.Add(roleVm);
};
return View(model);
}
HttpPost Account Register action:
// POST: /Account/Register
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser { FirstName = model.FirstName, LastName = model.LastName, PhoneNumber = model.PhoneNumber, UserName = model.Email, Email = model.Email};
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
//populate the roles checkbox list
var rolesSelectedOnView = model.RolesList.ToList();
foreach (var role in rolesSelectedOnView)
{
var roleVm = new ApplicationRoleRegisterViewModel
{
Name = role.Name,
IsSelected = role.IsSelected,
IsDisabled = role.IsDisabled
};
model.RolesList.Add(roleVm);
};
var rolesAddResult = await UserManager.AddToRolesAsync(user.Id, rolesSelectedOnView.Select(r => r.Name).ToArray());
if (!rolesAddResult.Succeeded)
{
ModelState.AddModelError("", rolesAddResult.Errors.First());
AddErrors(rolesAddResult);
return View(model);
}
string callbackUrl = await SendEmailConfirmationTokenAsync(user.Id, "Confirm your account");
// Uncomment to debug locally
// TempData["ViewBagLink"] = callbackUrl;
ViewBag.Message = "A confirmation email has been sent to the address you specified. Please have "
+ "the person check their email and confirm their account. The account must be confirmed "
+ "from the confirmation email before they can log in.";
return View("Info");
//return RedirectToAction("Index", "Home");
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
Register View (uses RegisterViewModel):
<div class="form-group">
@Html.Label("Roles", new { @class = "col-md-offset-2 col-md-10" })
<span class="col-md-offset-2 col-md-10">
@for (var i = 0; i < Model.RolesList.Count; i++)
{
@Html.HiddenFor(m => m.RolesList[i].Name)
@Html.CheckBoxFor(m => m.RolesList[i].IsSelected)
@Html.LabelFor(m => m.RolesList[i].IsSelected, Model.RolesList[i].Name)
<br />
}
</span>
</div>