0

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>
Bill Kron
  • 113
  • 13
  • 1
    You cant bind a checkbox to a collection of complex objects (which is what `IEnumerable` is. You need an property `IEnumerable` SelectedRoles` (and in any case `SelectListItem` is for use in a dropdown list, not for check boxes). Refer [this answer](http://stackoverflow.com/questions/29542107/pass-list-of-checkboxes-into-view-and-pull-out-ienumerable/29554416#29554416) for how to implement this correctly) –  Jun 23 '16 at 23:24
  • @StephenMuecke Thank you for the direction. I greatly appreciate it! – Bill Kron Jun 24 '16 at 00:59
  • @StephenMuecke I just took a look at your example and it's clear and makes sense. I have a follow up question: in my case (user registration), I won't have a user to associate with the selected roles until the HttpPost method starts. Can you give me a little more direction about how that changes for this situation? – Bill Kron Jun 24 '16 at 19:22
  • @StephenMuecke I was able to get this almost working with your previous answer that you pointed me to (thank you!). There are two problems that remain: first, when I submit the form, the roles checkbox list is not required. The second problem is possibly related: the selected values are not getting picked up correctly. The user is created in the database, but rather than the user being added to only the selected roles, they are added to all of the roles. Would you be willing to look at my updated code? – Bill Kron Jun 25 '16 at 07:14
  • Sure. Either ask a new question edit this question with what you have tried. –  Jun 25 '16 at 07:18
  • @StephenMuecke Thank you so much! I posted the revised code under **UPDATE #1** above. – Bill Kron Jun 25 '16 at 07:34
  • No time just now, but will have a look in an hour or so. –  Jun 25 '16 at 07:37
  • 1
    The code in the GET method and view looks fine (although your GET code can be simplified), but I don't understand what your trying to do in the POST method. All you doing is building a new collection and replacing the existing collection with what it already is. All you would need is `var rolesAddResult = await UserManager.AddToRolesAsync(user.Id, model.Roles.Where(r => r.IsSelected).Select(r => r.Name).ToArray());` assuming you want to pass the selected role names. –  Jun 25 '16 at 08:17
  • 1
    Also not sure what the `IsDisabled` property is for (you don't generate a form control for it so it will always be `null` in the POST method) –  Jun 25 '16 at 08:19
  • @StephenMuecke This is my first ASP.Anything project, so I'm still learning how all of this works. I made your suggested change in the POST method and now the roles are saving correctly. Awesome! – Bill Kron Jun 25 '16 at 18:06
  • @StephenMuecke What I was thinking with the IsDisabled property, is that I don't want someone who is not an admin to assign the admin role to someone else. However, I would like an admin to be able to do that. You're right, though, I do need to add that to the form. Not sure if that's the best way to handle it, but that's what came to mind. – Bill Kron Jun 25 '16 at 18:09
  • @StephenMuecke The only thing I'm still scratching my head about, is why the roles list doesn't seem to be part of the form validation. If I fill in none of the fields and click the submit button I get the validation summary for All of the fields, except for the roles list. Do you have any thoughts on that? – Bill Kron Jun 25 '16 at 18:11
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/115611/discussion-between-stephen-muecke-and-bill-kron). –  Jun 26 '16 at 00:48

0 Answers0