MVC5 Application: Create Identity User results in Invalid Model State? - asp.net-mvc

I am upgrading an application in Development from MVC4/EF5 to MVC5/EF6 to make use of (among other things) ASP.Net Identity. When I try to Create a User, my code is flagging the Model as Invalid and not creating the user. My View is simply displaying a box to enter an email, and then a Switch that lets the logged in admin select either an MemberOrganization or Sponsor to assign the new user 2 via some dropdowns.
The Create() method of my UserController is below:
// GET: Admin/UserManagement/Create
public ActionResult Create()
{
ViewBag.headerTitle = "Create User";
ViewData["Organization"] = new SelectList(db.MemberOrganizations, "Id", "Name");
ViewData["Sponsor"] = new SelectList(db.SponsorOrganizations, "Id", "Name");
ViewBag.SwitchState = true;
ApplicationUser newUser = new ApplicationUser();
newUser.RegisteredDate = DateTime.Now;
newUser.LastVisitDate = DateTime.Now;
newUser.ProfilePictureSrc = null;
return View(newUser);
}
// POST: Admin/UserManagement/Create
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create([Bind(Include = "Property1, Property2, etc.")] ApplicationUser applicationUser)
{
if (ModelState.IsValid)
{
ViewBag.headerTitle = "Create User";
PasswordHasher ph = new PasswordHasher();
var password = ph.HashPassword("aR#nD0MP#s$w0r9");
var user = new ApplicationUser() { UserName = applicationUser.UserName, Email = applicationUser.Email, PasswordHash = password };
IdentityResult result = await UserManager.CreateAsync(user, user.PasswordHash);
if (result.Succeeded)
{
await db.SaveChangesAsync();
return RedirectToAction("Index", "UserManagement");
}
else
{
ModelState.AddModelError("", "Failed to Create User.");
}
}
ModelState.AddModelError("", "Failed to Create User.");
var errors = ModelState.Where(x => x.Value.Errors.Count > 0).Select(x => new { x.Key, x.Value.Errors }).ToArray();
var errors2 = ModelState.Values.SelectMany(v => v.Errors);
ViewData["Organization"] = new SelectList(db.MemberOrganizations, "Id", "Name", applicationUser.MemberOrgId);
ViewData["Sponsor"] = new SelectList(db.SponsorOrganizations, "Id", "Name", applicationUser.SponsorOrgId);
if (applicationUser.MemberOrgId != null)
{
ViewBag.SwitchState = true;
}
else
{
ViewBag.SwitchState = false;
}
ViewBag.OrganizationId = new SelectList(db.MemberOrganizations, "Id", "State", applicationUser.MemberOrgId);
// If we got this far, something failed, redisplay form
return View(applicationUser);
}
In my attempts to debug the issue I added the errors/errors2 variables as suggested in this post. Going down into the Model State properties when these are flagged I receive:
Does anyone have some thoughts on this matter? My previous code was working fine but I'm still getting use to ASP.Net Identity.
EDIT: As suggested by Rikard I have set my model where SponsorOrgID and MemberOrgID are not both required (only 1). Now my code processes down until the following segment:
var user = new ApplicationUser() { Name = applicationUser.Name, Email = applicationUser.Email, PasswordHash = password };
IdentityResult result = await UserManager.CreateAsync(user, user.PasswordHash);
if (result.Succeeded) // ERROR
{
await db.SaveChangesAsync();
return RedirectToAction("Index", "UserManagement");
}
When I check the value of result and drill down to Errors->[string[]]->[0] the error message is: Name cannot be null or empty. Anyone have thoughts on this? I added a field to my View to specify the new users Name and incorporated it into the above new ApplicationUser() code line. I'm not fully sure where I am missing something.
EDIT2:
Create() View [Relevant]:
#model PROJECTS.Models.ApplicationUser
#{
ViewBag.Title = "Create";
Layout = "~/Areas/Admin/Views/Shared/_LayoutAdmin.cshtml";
string cancelEditUrl = "/Admin/UserManagement/";
}
#using (Html.BeginForm("Create", "UserManagement", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
#Html.HiddenFor(model => model.RegisteredDate)
<div class="container">
<div class="row">
<div class="editor-label">
#Html.LabelFor(model => model.Name)
</div>
<div class="editor-field" style="margin-bottom: 15px">
#Html.TextBoxFor(model => model.Name, new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.Name)
</div>
</div>
<div class="row">
<div class="editor-label">
#Html.LabelFor(model => model.Email)
</div>
<div class="editor-field" style="margin-bottom: 15px">
#Html.TextBoxFor(model => model.Email, new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.Email)
</div>
</div>
....

As you can se in your last picture you have an error on the property SponsorOrgId that has the value string.Empty (""). Maybe the SponsorOrgId in ApplicationUser has the [Requried] attribute.
EDIT
Regarding your problem when trying to add the user to the Database (that was happen when you call UserManager.Create(user,password);
IdentityResult result = await UserManager.CreateAsync(user, user.PasswordHash);
if (result.Succeeded)
{
await db.SaveChangesAsync();
return RedirectToAction("Index", "UserManagement");
}
else
{
var errors = string.Join(",", result.Errors);
ModelState.AddModelError("", errors);
}
Then you can debug the value of "errors" or read the error message from your ModelState.
Regarding your EDIT
Add name to this part:
var user = new ApplicationUser() { UserName = applicationUser.UserName, Email = applicationUser.Email, PasswordHash = password, Name = applicationUser.Name };
EDIT 2
The problem is that is not possible to create a user without a username. But you can add the user's email to the username. And then change it to the user specified username. To make it pass the validation you need to add this part.
UserManager.UserValidator = new UserValidator<User>(UserManager) { RequireUniqueEmail = true };

I realize it's late for a reply, but I read four threads on this before resolving this issue. It's not entirely obvious, and it appears to be an inheritance conflict with custom properties. The root of my problem was my creation of a UserName property - a custom property (...or so I thought) that I wanted to define as FirstName + " " + LastName.
public class ApplicationUser : IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
// public new string UserName { get; set; }
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
...remainder removed for clarity.
As soon as I commented-out this line from IdentityModels.cs, which removed my custom property, POOF! Problem: solved. I drilled inheritance definitions all the way back to IUser[TKey], and I found what I think was the root of my (our) problem.
namespace Microsoft.AspNet.Identity
{
// Summary:
// Minimal interface for a user with id and username
//
// Type parameters:
// TKey:
public interface IUser<out TKey>
{
// Summary:
// Unique key for the user
TKey Id { get; }
//
// Summary:
// Unique username
string UserName { get; set; }
}
}
I knew you could add custom properties to the ApplicationUser class, but I didn't know there were specific property names already in use - not defined in this class. Initially, after adding my UserName field simply as a public string, I received this:
[...] warning CS0114: 'MyApp.Models.ApplicationUser.UserName' hides inherited member 'Microsoft.AspNet.Identity.EntityFramework.IdentityUser<string,Microsoft.AspNet.Identity.EntityFramework.IdentityUserLogin,Microsoft.AspNet.Identity.EntityFramework.IdentityUserRole,Microsoft.AspNet.Identity.EntityFramework.IdentityUserClaim>.UserName'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.
I'm good at following instructions, so I added the 'new' keyword (remember that line above I had to comment-out...?). It solved the compile time CS0114 warning, but it obstructed IUser's UserName.
OP (and countless others) did the same thing, I believe, when he wrote this:
var user = new ApplicationUser() { UserName = applicationUser.UserName, Email = applicationUser.Email, PasswordHash = password };

If you have username and email properties in your applicationuser class, these properties are hiding actual properties, so remove them from your application class.This will solve the problem.

Related

Form submitting DropDownListFor with Required field

I have a dropdownlist from a table called StampsCSEMonths_test. Im trying to make the dropdown required before submitting the form.
The problem it always submits the form no matter if I select or not.
What could be the issue?
View:
#model WINHRAdmin.Models.CTS_Stamps
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
#Html.DropDownListFor(model => model.stampscsemonths_id, ViewBag.StampsCSEMonths_test as IEnumerable<SelectListItem>, "", new { #class = "form-control " })
#Html.ValidationMessageFor(model => model.stampscsemonths_id, "", new { #class = "text-danger" })
}
Model:
namespace WINHRAdmin.Models
{
public class CommitmentToServiceModel
{
}
public class CTS_Stamps
{
[Required]
[Range(1,Int32.MaxValue,ErrorMessage ="MonthYerar is required")]
public int? stampscsemonths_id { get; set; }
}
}
Controller:
public ActionResult CTS_Stamps()
{
ViewData["StampsCSEMonths_test"] =
new SelectList((from s in _IntranetEntities.StampsCSEMonths_test.OrderByDescending(x => x.stampscsemonths_id).ToList()
select new
{
stampscsemonths_id = s.stampscsemonths_id,
FullName = s.month + "/" + s.year
}),
"stampscsemonths_id",
"FullName",
null);
LoadEmployeer();
return View();
}
[HttpPost]
public ActionResult CTS_Stamps(FormCollection collection)
{
var testvar = collection["stampscsemonths_id"];//after submission it does get the value of ID
return View();
}
Typically you would want to use a client-side library like jquery.validate to prevent form submission if it's invalid. Here is a good article on the subject: https://www.codeproject.com/Articles/718004/ASP-NET-MVC-Client-Side-Validation.
The usual pattern for server-side MVC validation is almost what you're doing. You put the [Required] attribute above your data model attribute. That's good. You have a #ValidationMessageFor element in your view. Also good.
You forgot, however, to check if the model is valid in your controller. Add a check for ModelState.IsValid before you process the results. Something like this:
if (!ModelState.IsValid)
{
return View(collection); // assumes collection is the model
}
If the user submits an invalid form, after the round-trip they will see a validation error message next to the missing field.
Hope that helps.

Dropdownlist value is null after posting to controller in ASP.NET MVC

I can get all Roles plus actually Role for chosed user, but then When I posting to EditUser action, then Dropdownlist sends null.
I mean When the form posts to my controller, I get null from DropDownList.
Here is my Model
public class EditUserViewModel
{
public string Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public List<SelectListItem> ApplicationRoles { get; set; }
public string ApplicationRoleId { get; set; }
}
Here is Action
[HttpGet]
public async Task<ActionResult> EditUser(string id)
{
EditUserViewModel model = new EditUserViewModel();
model.ApplicationRoles = RoleManager.Roles.Select(r => new SelectListItem
{
Text = r.Name,
Value = r.Id
}).ToList();
if (!String.IsNullOrEmpty(id))
{
ApplicationUser user = await UserManager.FindByIdAsync(id);
if (user != null)
{
var role = await UserManager.GetRolesAsync(user.Id);
var existingRole = role.First();
string existingRoleId = RoleManager.Roles.Single(r => r.Name == existingRole).Id;
model.Id = user.Id;
model.FirstName = user.FirstName;
model.ApplicationRoleId = existingRoleId;
ViewBag.RoleId = new SelectList(RoleManager.Roles, "Id", "Name", model.ApplicationRoleId);
}
}
return PartialView("_EditUser", model);
}
And here is DropDownlist from _EditUser.cshtml
<div class="form-group">
#Html.Label("Role typ", htmlAttributes: new { #class = "control-label col-md-6" })
<div class="col-md-12" title="Ange antal datorer som finns i lager">
#Html.DropDownList("RoleId", null, new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.ApplicationRoles, "", new { #class = "text-danger" })
</div>
</div>
Getting null Only from DropDownList, not from #Html.EditorFor
/Thanks in advance!
Forms post back the name/value pairs of their successful form controls. Your generating a <select> element with name="RoleId" but you model does not contain a property named RoleId. Since you want to bind the selected option to the ApplicationRoleId role property, then you view needs to be
#Html.LabelFor(m => m.ApplicationRoleId)
#Html.DropDownListFor(m => m.ApplicationRoleId, Model.ApplicationRoles)
#Html.ValidationMessageFor(m => m.ApplicationRoleId)
Notes:
Your current #Html.Label(..) code does not create a label
associated with your dropdownlist (clicking on it will not set
focus)
The ValidationMessageFor() need to be applied to the property your
binding to, not the SelectList
Delete you ViewBag.RoleId = new SelectList(..) code. Your have
already assigned the selectlist to the ApplicationRoles property
(and you should never need ViewBag if have a view model anyway)
Because you are declare that only HttpGet methods are allow in that method of the controller. Thats why

Asp.net MVC ModelState.Isvalid returning false for Id

I'm watching this ASP.NET MVC course. I have a customer model with these following attribute.
public class Customer {
public int Id { get; set; }
[Required]
[StringLength(255)]
public string Name { get; set; }
[Display(Name = "Date of Birth")]
public DateTime? DateOfBirth { get; set; }
public bool IsSubscribedToNewsLetter { get; set; }
public MembershipType MembershipType { get; set; }
[Display(Name="Membership Type")]
public byte? MembershipTypeId { get; set; }
}
Note thate the Id has no Required data annotation. But in my database, the Id is primary key and Identity is true for the column.
There is a ViewModel consisting Customer and MembershipType models.
public class CustomerFormViewModel {
public IEnumerable<MembershipType> MembershipTypes { get; set; }
public Customer Customer { get; set; }
}
I have a View that creates new Customer with Name, DateOfBirth, MembershipType and IsSubscribedToNewsLetter fields. It takes the CustomerFormViewModel.
#using Vidly.Models
#model Vidly.ViewModel.CustomerFormViewModel
#{
ViewBag.Title = "New";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>#ViewBag.Message</h2>
#using (Html.BeginForm("Save", "Customer")) {
<div class="form-group">
#Html.LabelFor(m => m.Customer.Name,new{#class="control-label"})
#Html.TextBoxFor(m => m.Customer.Name, new { #class = "form-control" })
#Html.ValidationMessageFor(m=>m.Customer.Name)
</div>
<div class="form-group">
#Html.LabelFor(m => m.Customer.DateOfBirth, new { #class = "control-label"})
#Html.TextBoxFor(m => m.Customer.DateOfBirth,"{0:d MMM yyyy}", new { #class = "form-control" })
</div>
<div class="form-group">
#Html.LabelFor(m => m.Customer.MembershipTypeId, new { #class = "control-label"})
#Html.DropDownListFor(m => m.Customer.MembershipTypeId,new SelectList(Model.MembershipTypes,"Id","Name"), "Select Membership Type", new { #class = "form-control" })
</div>
<div class="checkbox">
<label>
#Html.CheckBoxFor(m => m.Customer.IsSubscribedToNewsLetter) Subscribed To Newsletter?
</label>
</div>
#Html.HiddenFor(m=>m.Customer.Id)
<button type="submit" class="btn btn-primary">Save</button>
}
Here is my Save controller:
public ActionResult Save(Customer customer) {
if (!ModelState.IsValid) {
var viewModel = new CustomerFormViewModel {
Customer = customer,
MembershipTypes = _context.MembershipTypes.ToList()
};
return View("CustomerForm", viewModel);
}
if (customer.Id == 0 || customer.Id==null) {
_context.Customers.Add(customer);
}
else {
var CustomerInDb = _context.Customers.Single(c => c.Id == customer.Id);
CustomerInDb.Name = customer.Name;
CustomerInDb.DateOfBirth = customer.DateOfBirth;
CustomerInDb.IsSubscribedToNewsLetter = customer.IsSubscribedToNewsLetter;
CustomerInDb.MembershipTypeId = customer.MembershipTypeId;
}
_context.SaveChanges();
return RedirectToAction("Index", "Customer");
}
When I fill the CustomerForm view and click the submit button, the ModelState.Isvalid() method always comes false; resulting the first if statement of the Save method true. So I can't store any new customer.
I tried to debug the application by putting breakpoint on if (!ModelState.IsValid) and saw that the Id field is creating a error(saying "The Id field is required"). Why is it saying that Id is required when it isn't? Does the ModelState.IsValid method check the model at database level? I don't think so.
If I change the Customer model's Id property like this:public int? Id { get; set; } and change the if statement by this,if ((!ModelState.IsValid) && (customer.Id==null) ) the application works fine.
Is there any other solution of this Id problem?
I watched the same course and I am guessing the author updated since you watched it, as he demonstrated this exact type of issue. The issue is that when returning the View Model to the View on the New Action, the Customer property is not initialized so the Id is null hence the ModelState failure when trying to Save
Just change as below, so that when setting the viewModel, you initialize the Customer and the Id is then 0:
public ActionResult New()
{
var memberShipTypes = _context.MembershipTypes.ToList();
var viewModel = new CustomerViewModel
{
Customer = new Customer(),
MembershipTypes = memberShipTypes
};
return View("CustomerForm", viewModel);
}
Had a heck of a time with this, and created a workaround, but it seems that Mosh addresses this in a later section where he sets up the Movie Form.
https://codewithmosh.com/courses/222293/lectures/3684111
The short answer is to add #Html.Hidden("Movie.Id", (Model.Movie != null) ? Model.Movie.Id : 0) to the MovieForm.cshtml.
He then describes a way to avoid hard-coding "Movie.Id" into the view (see https://github.com/mosh-hamedani/vidly-mvc-5/commit/e5b994581931a079ad87418ddcf9338e808bd821#diff-e94a8dc96403203b00e58238bb80101c )
This is just a rough draft as I don't have access to VS right now. Anyway, modify your Save action like so:
public ActionResult Save(CustomerFormViewModel customerVM) {
if (!ModelState.IsValid) {
return View(customerVM);
}
if (customer.Id == 0) {
_context.Customers.Add(customerVM.Customer);
}
else {
var CustomerInDb = _context.Customers.Find(customerVM.Customer.Id);
CustomerInDb.Name = customer.Name;
CustomerInDb.DateOfBirth = customer.DateOfBirth;
CustomerInDb.IsSubscribedToNewsLetter = customer.IsSubscribedToNewsLetter;
CustomerInDb.MembershipTypeId = customer.MembershipTypeId;
}
_context.SaveChanges();
return RedirectToAction("Index", "Customer");
}
Oh and you can remove the following from the view since this is for create page:
#Html.HiddenFor(m=>m.Customer.Id)
hey change Id to CustomerId in model class.
i think just 'Id' may be treated as Primary Key of that model class.
we are doing the same course and i ran into the exact same problem lol.
i found a pretty nice workaround in my opinion.
just add these lines of code in the CustomerFormView.
instead of
#Html.HiddenFor(m=>m.Customer.Id)
Add:
{
if (Model.Customer == null)
{
<input data-val="true" data-val-number="The field Id must be a number." data-val-required="The Id field is required." id="Customer_Id" name="Customer.Id" type="hidden" value="0" />
}
else
{
#Html.HiddenFor(m => m.Customer.Id)
}
}
for some reason i saw that when i try to add a new customer , the value of id is an empty string instead of zero.
therefore i changed it to zero manually in case the Customer object is null
(which will always be the case when adding a new customer.)
and it works fine for me.
let me know if you think this solution is problematic..
BTW Regarding your question : "the Id field is creating a error(saying "The Id field is required"). Why is it saying that Id is required when it isn't?"
Int data type is non nullable therefore it is implicitly required..same as the MembershipId (byte data type that doesnt have the [Required] annotation.)
I am also going through this course. i have got a same issue. Add this line in the customerviewform
if (Model.Customers !=null)
{
#Html.HiddenFor(m => m.Customers.Id)
}
Why i have add because while hiddenfor which it is used for editing purpose if you remove also it will be problem. So add this line it will we be work and in customer model add public int? Membershiptype. And another thing while adding a new customer if you got error at dropdownlist then add this line before validation area return
customer.MembershipTypes = _Context.MembershipTypeTableset.ToList(); add this line before View("CustomerForm", viewModel)
This example is taken from Mosh Hamedani's MVC 5 course. He explained the Customer Id issue in the chapter 55. This can be resolved by passing a new customer() object in New method while creating a CustomerFormViewModel.
Perhaps the problem is that the Id field is marked as an int and not int?. Putting a variable as int the Model automatically assumes there's going to be a value for this property since it's not marked nullable.
Try marking the Id Property is int? and see if the results are what you expect or not.
After seeing this question, I made a workaround of my problem. I just disabled my Id error in ModelState at the very beginning of Save action.
public ActionResult Save(Customer customer) {
ModelState["customer.Id"].Errors.Clear();
if ((!ModelState.IsValid) ) {
var viewModel = new CustomerFormViewModel {
Customer = customer,
MembershipTypes = _context.MembershipTypes.ToList()
};
return View("CustomerForm", viewModel);
}
if (customer.Id == 0) {
_context.Customers.Add(customer);
}
else {
var CustomerInDb = _context.Customers.Single(c => c.Id == customer.Id);
CustomerInDb.Name = customer.Name;
CustomerInDb.DateOfBirth = customer.DateOfBirth;
CustomerInDb.IsSubscribedToNewsLetter = customer.IsSubscribedToNewsLetter;
CustomerInDb.MembershipTypeId = customer.MembershipTypeId;
}
_context.SaveChanges();
return RedirectToAction("Index", "Customer");
}
Now my Application works fine.

Adding a Roles Checkbox List to the Register Method of the ASP.NET MVC 5 Default Template

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>

MVC 5: Table edits appear to be an all-or-nothing proposition -- why?

I am using boilerplate CRUD methods, which include BIND operations on the methods that catch the form submissions. I have discovered that if I have my fields as nullable both in the model as well as in the DB, and I do not include those fields in a CRUD operation (also no presence in BIND), these fields end up null when before they were filled. If I flag these fields as not nullable in the DB, I cannot complete the CRUD operation without including these fields in hidden form fields because they are not nullable.
How do I make a CRUD operation ignore these fields without adding them as hidden fields in the forms?? As in, do not null them, do not change their data.
For example, if I have my POST method as such:
public async Task<ActionResult> Edit([Bind(Include = "CompanyId,CompanyName,CompanyAddress,CompanyCity")] Company company) {
And there is a field both in the model as well as in the DB such as CompanyCity, if it is nullable in model and db it gets nulled with the update. If it is not nullable in the model and db, the update fails because the field is not nullable but the update wants to null it because it didn't exist in the bind.
I am also using only the base models, such as Company, for this example. However when I try to make another base model, such as EditCompanyViewModel, I am unable to pull data out of the database to put into that view model. The entire await command gets flagged as being not of the correct model/type.
Essentially, I need to know how to edit only part of a table, without messing/mucking/deleting the rest of the table entries and without creating a metric arseload of hidden form fields that exist purely to hold the data I don't want to edit.
I have a conceptual gap here, and I am metaphorically chasing my tail. I can't seem to bridge the gap to a solution.
EDIT:
My modified view model:
public class EditMarketingViewModel {
[Key]
public Guid CompanyId { get; set; }
[DisplayName("How did you hear of us")]
public Guid? HowHeardId { get; set; }
[DisplayName("eNewsletter")]
public bool eNewsletter { get; set; }
[DisplayName("Event Code")]
public Guid? EventCodeId { get; set; }
[DisplayName("Notes")]
public string MarketingNotes { get; set; }
#region Essentials
[HiddenInput, Timestamp, ConcurrencyCheck]
public byte[] RowVersion { get; set; }
[HiddenInput]
public DateTime Modified { get; set; }
[HiddenInput]
public string TouchedBy { get; set; }
#endregion
}
My view:
#model CCS.Models.EditMarketingViewModel
#{
ViewBag.Title = "Edit Marketing Info.";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>#ViewBag.Title</h2>
#using(Html.BeginForm()) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<fieldset>
#Html.HiddenFor(model => model.CompanyId)
#Html.HiddenFor(model => model.RowVersion)
#Html.LabelFor(model => model.HowHeardId, htmlAttributes: new { #class = "control-label" })#Html.DropDownList("HowHeardId", null, " « ‹ Select a How Heard Type › » ", htmlAttributes: new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.HowHeardId, "", new { #class = "text-danger" })
#Html.LabelFor(model => model.eNewsletter, htmlAttributes: new { #class = "control-label" })#Html.EditorFor(model => model.eNewsletter, new { htmlAttributes = new { #data_on_text = "Yes", #data_off_text = "No" } })
#Html.ValidationMessageFor(model => model.eNewsletter, "", new { #class = "text-danger" })
#Html.LabelFor(model => model.EventCodeId, htmlAttributes: new { #class = "control-label" })#Html.DropDownList("EventCodeId", null, " « ‹ Select an Event Code › » ", htmlAttributes: new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.EventCodeId, "", new { #class = "text-danger" })
#Html.LabelFor(model => model.MarketingNotes, htmlAttributes: new { #class = "control-label" })#Html.EditorFor(model => model.MarketingNotes, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.MarketingNotes, "", new { #class = "text-danger" })
<input type="submit" value="Save" class="btn btn-default" />
</fieldset>
}
<p>[ #Html.ActionLink("Back to List", "Index", "Company") ]</p>
Now how do I modify my controller to work with it:
// GET: Company/EditMarketing
public async Task<ActionResult> EditMarketing() {
var id = new Guid(User.GetClaimValue("CWD-Company"));
Company company = await db.Company.FindAsync(id);
if(company == null) {
return HttpNotFound();
}
ViewBag.HowHeardId = new SelectList(db.HowHeard.Where(x => x.Active == true).OrderBy(x => x.SortOrder), "HowHeardId", "HowHeardType", company.HowHeardId);
ViewBag.EventCodeId = new SelectList(db.EventCode.Where(x => x.Active == true).OrderBy(x => x.EventCodeDate), "EventCodeId", "EventCodeName", company.EventCodeId);
return View(company);
}
EDIT 2:
My POST:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> EditMarketing([Bind(Include = "CompanyId,HowHeardId,eNewsletter,EventCodeId,MarketingNotes,RowVersion")] Company company) {
try {
if(ModelState.IsValid) {
TextInfo ti = CultureInfo.CurrentCulture.TextInfo;
company.Modified = DateTime.UtcNow;
company.TouchedBy = User.Identity.GetFullNameLF();
db.Entry(company).State = EntityState.Modified;
await db.SaveChangesAsync();
return RedirectToAction("Index", "Company");
}
} catch(DbUpdateConcurrencyException ex) {
var entry = ex.Entries.Single();
var companyValues = (Company)entry.Entity;
var databaseValues = (Company)entry.GetDatabaseValues().ToObject();
if(databaseValues.MarketingNotes != companyValues.MarketingNotes) { ModelState.AddModelError("MarketingNotes", "Current Value: " + databaseValues.MarketingNotes); }
if(databaseValues.eNewsletter != companyValues.eNewsletter) { ModelState.AddModelError("eNewsletter", "Current Value: " + databaseValues.eNewsletter); }
if(databaseValues.HowHeardId != companyValues.HowHeardId) { ModelState.AddModelError("HowHeardId", "Current Value: " + db.HowHeard.Find(databaseValues.HowHeardId).HowHeardType); }
if(databaseValues.EventCodeId != companyValues.EventCodeId) { ModelState.AddModelError("EventCodeId", "Current Value: " + db.EventCode.Find(databaseValues.EventCodeId).EventCodeName); }
ModelState.AddModelError(string.Empty, "The record you attempted to edit "
+ "was modified by another user after you got the original value. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again. Otherwise click the Back to List hyperlink.");
company.RowVersion = databaseValues.RowVersion;
} catch(DataException dex) {
ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists please inform your Manager, who will inform the developers." + dex);
}
ViewBag.HowHeardId = new SelectList(db.HowHeard.Where(x => x.Active == true).OrderBy(x => x.SortOrder), "HowHeardId", "HowHeardType", company.HowHeardId);
ViewBag.EventCodeId = new SelectList(db.EventCode.Where(x => x.Active == true).OrderBy(x => x.EventCodeDate), "EventCodeId", "EventCodeName", company.EventCodeId);
return View(company);
}
Please note that I am making use of concurrency to avoid data collisions. Hence the RowVersion column.
As I understand your code EditMarketingViewModel is used to update a company record. Pass your view model as parameter to your post action result. You would want to load the company record first before updating it like so. This approach makes your record retain property values which are not needed to be updated.
[HttpPost]
public async Task<ActionResult> Edit(EditMarketingViewModel viewModel)
{
if (!ModelState.IsValid)
{
ViewBag.HowHeardId = new SelectList(db.HowHeard.Where(x => x.Active == true).OrderBy(x => x.SortOrder), "HowHeardId", "HowHeardType", company.HowHeardId);
ViewBag.EventCodeId = new SelectList(db.EventCode.Where(x => x.Active == true).OrderBy(x => x.EventCodeDate), "EventCodeId", "EventCodeName", company.EventCodeId);
return View(viewModel);
}
Company company = await db.Company.FindAsync(viewModel.CompanyId);
if(company == null) {
return HttpNotFound();
}
company.HowHeardId = viewModel.HowHeardId;
company.eNewsletter = viewModel.eNewsletter;
// etc.
// don't need to assign a new value to properties that should be retained
db.Entry(company).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
Update:
You are returning an object of type Company from your GET controller but your view is expecting an EditMarketingViewModel. So do it like this:
// GET: Company/EditMarketing
public async Task<ActionResult> EditMarketing() {
var id = new Guid(User.GetClaimValue("CWD-Company"));
Company company = await db.Company.FindAsync(id);
if(company == null) {
return HttpNotFound();
}
ViewBag.HowHeardId = new SelectList(db.HowHeard.Where(x => x.Active == true).OrderBy(x => x.SortOrder), "HowHeardId", "HowHeardType", company.HowHeardId);
ViewBag.EventCodeId = new SelectList(db.EventCode.Where(x => x.Active == true).OrderBy(x => x.EventCodeDate), "EventCodeId", "EventCodeName", company.EventCodeId);
EditMarketingViewModel viewModel = new EditMarketingViewModel()
{
CompanyId = company.Id,
// Other view model properties go here
}
return View(viewModel);
}
Make sure the object type you are returning from the controller to the view matches.

Resources