Bind Model to Radio Buttons on POST - c#

I am trying to expand on the user register form that is generated using EF6 and MVC, I want to be able to display a list of user roles that is displayed as radio buttons, which I believe I have done. However the trouble I am having is when I post the form back to the controller, it's not binding the selected option back to the view model. What am I doing wrong?
My View Model:
public class RegisterViewModel
{
[Required]
[Display(ResourceType = typeof(ResourceStrings), Name = "Username")]
[StringLength(50, ErrorMessageResourceType = typeof(ResourceStrings), ErrorMessageResourceName = "MinLengthValidation", MinimumLength = 4)]
public string Username { get; set; }
[Required]
[Display(ResourceType = typeof(ResourceStrings), Name = "FirstName")]
[StringLength(50, ErrorMessageResourceType = typeof(ResourceStrings), ErrorMessageResourceName = "MinLengthValidation", MinimumLength = 2)]
public string FirstName { get; set; }
[Required]
[Display(ResourceType = typeof(ResourceStrings), Name = "Surname")]
[StringLength(50, ErrorMessageResourceType = typeof(ResourceStrings), ErrorMessageResourceName = "MinLengthValidation", MinimumLength = 2)]
public string Surname { get; set; }
//Other fields removed to save space
[Required]
[Display(Name = "Roles")]
public IEnumerable<IdentityRole> UserRoles { get; set; }
}
I have a list of Identity Roles that I want to pass to the view like so:
// GET: /Account/Register
[AllowAnonymous]
public ActionResult Register()
{
var model = new RegisterViewModel();
model.UserRoles = new RoleManager<IdentityRole>(new RoleStore<IdentityRole>()).Roles.ToList();
return View(model);
}
And on the view I display the roles like so:
<div class="panel-body">
#foreach (var item in Model.UserRoles)
{
<div class="col-md-8">
<div>
#Html.RadioButtonFor(m => m.UserRoles, item.Name)
#Html.DisplayFor(m => item.Name, new { #class = "col-md-3 control-label" })
</div>
</div>
}
#Html.ValidationMessageFor(m => m.UserRoles, "", new { #class = "text-danger" })
</div>
However when the form is submitted back the model is never valid as the role is never bound. (Or at least that's what I believe) Any ideas?

You do not want to have the radio buttons tied to the list of values, you need to change you model to a list of roles and the selected role like this
//Other fields removed to save space
[Required]
[Display(Name = "Role")]
public IdentityRole UserRole { get; set; }
[Display(Name = "Roles")]
public IEnumerable<IdentityRole> UserRoles { get; set; }
Then you create the radio button like this
#Html.RadioButtonFor(m => m.UserRole, item.Name)
This will then post back the selected value to the UserRole property.
NOTE: I think you will also need to play with the value of the radiobutton to get it to populate back to the UserRole since I do not think item.Name would work. You may want to post back to a string field with the name and then look up the proper role in the post method of the controller.

I looked into this (Even tested the theory since I had one available) and it is impossible. You can bind any sub property of the selected object to your view model but not the object in its entirety.
Here is my reference from stack overflow.

Html controls (input, textarea, select) post back their name/value pairs. In your case the form data associated with the radio button would be UserRoles: someValue.
Assuming your POST method is public ActionResult Register(RegisterViewModel model), the DefaultModelBinder first initializes an instance of RegisterViewModel then finds the matching form data name/value pair and tries to set the value of property UserRoles to the string "someValue" which of course fails because UserRoles is a complex object and UserRoles becomes null.
You cannot bind html controls to complex objects unless you create a custom ModelBinder and/or ValueProviders and even then it will only set one property of you model because you are only posting back one value.
You need to create a view model with properties representing what you want to display/edit in the view, for example (assuming typeof IdentityRole has a property int ID)
public class RegisterViewModel
{
[Required]
[Display(ResourceType = typeof(ResourceStrings), Name = "Username")]
[StringLength(50, ErrorMessageResourceType = typeof(ResourceStrings), ErrorMessageResourceName = "MinLengthValidation", MinimumLength = 4)]
public string Username { get; set; }
....
// For binding the selected roles
[Required(ErrorMessage = "Please select a role")]
public int ID SelectedRole { set; set; }
// For displaying the available roles
[Display(Name = "Roles")]
public IEnumerable<IdentityRole> UserRoles { get; set; }
}
Then in the view
#model yourAssembly.RegisterViewModel
#using (Html.BeginForm())
{
....
foreach(var role in Model.UserRoles)
{
#Html.RadioButtonFor(m => m.SelectedRole, role.ID, new { id = role.Name })
<label for="#role.Name">#role.Name</label>
}
#Html.ValidationMessageFor(m => m.SelectedRole)
....
}
and when you post back, you can get the ID of the selected role from the models SelectedRole property.

Related

How to validate a unique property while user giving input (code first approach in .Net, MVC5)?

Model Class:
public class Course
{
[Key]
public int Id { get; set; }
[MinLength(5, ErrorMessage = "Code must be (5) characters long")]
[Index(IsUnique = true)]
[Column(TypeName = "VARCHAR")]
[Required]
[Display(Name = "Code")]
public string CourseCode { get; set; }
[Index(IsUnique = true)]
[Column(TypeName = "VARCHAR")]
[Required]
[Display(Name = "Name")]
[Remote("IsCourseNameExist", "Courses", HttpMethod = "POST", ErrorMessage = "Course is existed.")]
public string CourseName { get; set; }
//[System.ComponentModel.DataAnnotations.Compare("CourseName", ErrorMessage = "Already this Course is exist.")]
//[NotMapped]
//public string VeryfyName { get; set; }
[Range(0.5, 5, ErrorMessage = "Credit Must be between (0.5) to (5.0)")]
[Display(Name = "Credit")]
public decimal CourseCredit { get; set; }
public string Description { get; set; }
public int DepartmentId { get; set; }
public int SemesterId { get; set; }
[ForeignKey("DepartmentId")]
public virtual Department Department { get; set; }
[ForeignKey("SemesterId")]
public virtual Semester Semester { get; set; }
}
In Controller Class:
public JsonResult IsCourseNameExist(string CourseName)
{
//var course = .....
return Json(course == null);
}
In View:
<div class="form-group">
#Html.LabelFor(model => model.CourseName, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.CourseName, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.CourseName, "", new { #class = "text-danger" })
</div>
</div>
Description:
I am using Code First EF and very new in this area.
I have a Model class "Course" which generated a DB table named "Courses".
In view, user will add courses for a selected department and semester. But the Course Name property is unique.If user give a course name already exist in "Courses" table and submit button, then some error generates. That's why I want to make confirm that the user can not put any existing course name. So it needs checking before submission.
I have searched a lot, as I am newbie, everything is not clear to me. I found a way to use [Remote()] in model class and then use an action in controller to solve this. But cant apply it.
I expect some brief how to write the code that I mentioned in Controller class and what additional things need to add in view.
Thanks!
You can use a conditional statement combined with .Any() lambda expression.
public JsonResult IsCourseNameExist(string CourseName)
{
if(dbContext.Courses.Any(x => x.CourseName.Trim().ToUpper().Equals(CourseName.Trim().ToUpper())
{
return Json(false);
}
else
{
return Json(true);
}
}
Using .ToUpper() will help this be more efficient, because if your table you have a course name called Math 101.. and the user types in math 101 that might be submitted without error.
Let me know if this helps.

Send dropdownlist value back to variable MVC 4

Hi there I have a table of jobs with a specific id for them
id title
1 Manager
2 Engineer
3 IT
I have created a way so that I am able to load these value from a database with this table and data.
Here is my model
[Display(Name = "Type: "),
Required(ErrorMessage = "Required")]
public IEnumerable<SelectListItem> jobTitle { get; set; }
Here is my controller
public void loadDropDown()
{
JobModel selectedJob = new JobModel();
reviewlogsEntities db = new reviewlogsEntities();
IEnumerable<SelectListItem> type = db.types.Select(m => new SelectListItem
{
Value = SqlFunctions.StringConvert((double)m.id),
Text = m.title
});
ViewBag.JobTitle = type;
}
[HttpGet]
public ActionResult DisplayJobTitle()
{
if (Request.IsAuthenticated)
{
loadDropDown();
return View();
}
else
{
return RedirectToAction("index", "home");
}
}
[HttpPost]
public ActionResult DisplayJobTitle(JobModel curJob)
{
loadDropDown();
if (ModelState.IsValid)
{
return View();
}
else
{
ModelState.AddModelError("", "Looks like there was an error with your job title, please make sure a job title is selected.");
}
return View();
}
And Lastly what my view looks like:
#Html.ValidationSummary(true, "Please try again.")
#using (Html.BeginForm())
{
#Html.LabelFor(model => model.type)
#Html.DropDownList("JobTitle")
<input class="submitButton" type="submit" value="Show Job Info" style="margin-left:126px;margin-bottom: 20px;" />
}
See the problem is that variable jobTitle in my model is null because I never give it a value and since I don't give it a value the form thinks it is not filled out since it must be required and will not submit properly. My question is how do I return the value of whatever the users selected as their job title back to jobTitle variable when the form is submitted so that I won't get a submission failure.
Model should accept selected value not collection of items:
[Display(Name = "Type: "),
Required(ErrorMessage = "Required")]
public int jobTitle { get; set; }
You need a property to map selected value in your model like:
[Display(Name = "Type: "),
Required(ErrorMessage = "Required")]
public int SelectedjobTitle { get; set; }
and then in your View:
#Html.DropDownList("SelectedJobTitle",ViewBag.JobTile as IEnumerable<SelectListItem>)
use the DropDownListFor like this:
#Html.DropDownListFor(model => model.JobTitle, Model.Jobs)
on your model:
[Display(Name = "Type: "),
Required(ErrorMessage = "Required")]
public int JobTitle { get; set; }
public IEnumerable<SelectListItem> Jobs { get; set; }
don't forget to fill the "Jobs" on your controller.

Populate mvc dropdownlist based on user role

I'm adding a dropdown list to the view on my MVC project. The dropdown list will be populated based on the user type; a user can create a new user with the same or a lower access level. The access levels are based on the user 'role' assigned when the user is authenticated.
So for example, an administrator has the highest access level. He / she can create any user type, i.e.
Administrator
Manager
Supervisor
CSR
ReadOnly
A manager can create a manager, supervisor etc. (And so on.)
I have a 'Register' view where the user will enter the details to create a new user - username / password and access level. A dropdown list will give them the list of user types they can create.
My question is this: How should I populate the dropdown list?
I have the logic in my controller and I create the dropdown list and pass it to the view. But the problem is, as expected - when 'Register' is clicked, the dropdown list will be lost.
I can create the dropdown list in the view, but is this 'best' practice?
I have my code below for reference; any suggestions are greatly appreciated.
In the ViewModel:
public class RegisterViewModel
{
[Required]
[Display(Name = "User name")]
public string UserName { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
[Required]
[Display(Name = "User type")]
public IEnumerable<System.Web.Mvc.SelectListItem> UserType { get; set; }
}
View:
#Html.LabelFor(m => m.UserType, new { #class = "col-md-2 control-label" })
<div class="col-md-10">
#Html.DropDownListFor(m => m.UserType, Model.UserType, new { #class = "form-control" })
In the controller:
public ActionResult Register()
{
CSR.Models.ViewModels.Account.RegisterViewModel rvm = new CSR.Models.ViewModels.Account.RegisterViewModel();
rvm.UserType = GetRoles();
return View(rvm);
}
private IEnumerable<SelectListItem> GetRoles()
{
var roles = RolesList().Select(x => new SelectListItem { Value = x.accessLevelID.ToString(), Text = x.accessLevel });
return new SelectList(roles, "Value", "Text");
}
The RolesList() method returns a list of users based on User.IsInRole.
I am thinking the logic could be in the view so that the dropdown list is populated each time, but in my inexperienced opinion this seems to go against the 'MVC' pattern.
Advice is very much appreciated, thank you.
You can use this:
Create ActionFilter PopulateUserRoles (this filter adds to ViewData[Constants.UserTypes] collection of items where Constants.UserTypes is string constant "UserTypes"); example of such filter and you can inject inside filter everything you need
Decorate your Action by this attribute
Result:[PopulateUserRoles]
public ActionResult Register()...
...
#Html.DropDownListFor(x => x.UserType, ViewData[Constants.UserTypes]...
This option isn't required additional GetRoles() method in controller and additional IEnumerable UserType property in viewmodel. This option could be improved by using UIHint.

#Html.DropDownListFor fails in partial view but not in Full Page View

I am having an issue where my PartialView DropDownListFor gets the error:
The ViewData item that has the key PlanId is of type System.int32 but must be of type
IEnumerable<SelectListItem>
#Html.DropDownListFor(model => model.PlanId, (SelectList)ViewBar.PlanNameSelectList, new {#class = "short" })
This error does not pop up when I go to the page that originally held this code. What I have done is gutted the core part of the code which has worked previously with another partialView, as long as I took out the DropDownListFor elements in the code. I did not need them for that partialView, but now that I need them the problem has come full circle.
I would greatly appreciate any help that can be given to me to help solve this problem. Other resources like calls to the partial are below
#Html.Partial("location", new MAO.Models.ViewModels.CreateTemplateModel{})
This is the model
public class CreateTemplateModel {
[Required(ErrorMessage = "{0} is required.")]
[RegularExpression("^[0-9]+$", ErrorMessage="Template Id can only contain numbers")]
[Display(Name = "Template ID")]
public string TNumber { get; set; }
[Required(ErrorMessage = "{0} is required.")]
[RegularExpression("^.[0-9]{4}(-[0-9]{3})?$", ErrorMessage = "H# Must follow either #XXXX or #XXXX-XXX pattern")]
[Display(Name = "HNumber")]
public string HNumber { get; set; }
[RequiredIfOtherIsEmpty("NewPlanName", ErrorMessage = "Please enter a Plan Name")]
[Display(Name = "Select Existing Plan Name")]
public int PlanId { get; set; }
[MaxLength(500, ErrorMessage="{0} can't be longer than 500 characters")]
[Display(Name = "Enter New Plan Name")]
public string NewPlanName { get; set; }
[RequiredIfOtherIsEmpty("NewParentOrganization", ErrorMessage = "Please enter a Parent Organization")]
[Display(Name = "Select Existing Parent Organization")]
public string ParentOrganization { get; set; }
[MaxLength(500, ErrorMessage = "{0} can't be longer than 500 characters")]
[Display(Name = "Enter New Parent Organization")]
public string NewParentOrganization { get; set; }
[Required(ErrorMessage = "{0} is required.")]
public int TemplateTypeId { get; set; }
}
There is a controller that is pretty long so I am not going to post that. If there are parts of the controller that would be helpful I can post those parts as well as anything else that I might have forgotten to include
Based on your comments, I'm suspecting that you're never rebinding your drop down list when you are returning your partial view. Your controller action for the partial should be building your dropdown list in an identical manner to the controller action that renders the full view. Compare the two and make sure that they match.
UPDATE: Your partial view action should look something like the following:
public ActionResult Location()
{
ViewBag.PlanNameSelectList = new SelectList(plans.Distinct(), "Id", "Name", plans.FirstOrDefault(plan => plan.Name == selectedPlan));
attachSelectLists(ViewBag);
return PartialView("Location");
}
What you are currently doing with
#Html.Partial("location", new MAO.Models.ViewModels.CreateTemplateModel{})
Is rendering the partial view "location" using a NEW CreateTemplateModel object, not an existing one. Instead, a better way to do it is to duplicate your controller actions. Create a new one specifically for your partial view (this is a simpler use case for now).
public ActonResult TestPartialView()
Instead of using #Html.Partial which renders a partial, try calling your new controller action instead, which will build your drop down list for you.
#Html.RenderAction("TestPartialView").
This will call your new controller action and render the partial on the page, preserving the controller logic. If you use #Html.Partial, it simply renders the partial view passing in whatever object you give it which, in this case, is a new, empty CreateTemplateModel.

Partial Model Validation in MVC4

I have a PartialView that is a form used to create or modify a user and implements the ViewModel DynamicActionUserModel. The view that referrences this partialView shows a table of all MembershipUsers and gives the ability to create a new user Membership.CreateUser() or modify a user 'Membership.UpdateUser()`. The form in the partialView does an ajax post to my controller to submit data.
The issue I'm running into is that when a user is created their userName, email, password and role is serialized back to the controller as DynamicActionUserModel.RegisterModel and validated, but when a user is modified, password is not a property that is available (nor do I want to make it available to modify on the client side) so it isn't set in DynamicActionUserModel.RegisterModel and ModelState.IsValid is always false.
Perhaps the design of my model and view needs to change, or is there a way to validate the model but ignore password when a user is being modified? Not sure what the best practice is for this one.
I guess another option would be to create another ViewModel and another partialView specifically for modifying a user but that seems sloppy.
Model
public class DynamicActionUserModel {
public string Action { get; set; }
public RegisterModel RegisterModel { get; set; }
}
public class RegisterModel {
[Required]
[Display(Name = "User Name")]
public string UserName { get; set; }
[Required]
[DataType(DataType.EmailAddress)]
[Display(Name = "Email")]
public string Email { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
public string[] SelectedRoles { get; set; }
public MultiSelectList Roles { get; set; }
}
Controller
[HttpGet]
public ActionResult CreateUser() {
var model = new DynamicActionUserModel {
Action = "CreateUser",
RegisterModel = new RegisterModel {
Roles = new MultiSelectList(System.Web.Security.Roles.GetAllRoles())
}
};
return PartialView("_UserPartial", model);
}
[HttpGet]
public ActionResult ModifyUser() {
var model = new DynamicActionUserModel {
Action = "ModifyUser",
RegisterModel = new RegisterModel {
Roles = new MultiSelectList(System.Web.Security.Roles.GetAllRoles())
}
};
return PartialView("_UserPartial", model);
}
[HttpPost]
public ActionResult ModifyUser(DynamicActionUserModel model) {
bool isEqual = true;
if(!ModelState.IsValid) { // this is always false because password is empty
return PartialView("_UserPartial", model);
}
var user = Membership.GetUser(model.RegisterModel.UserName);
// do stuff
Membership.UpdateUser(user);
return Json(new {success = false});
}
View
#using RobotDog.Models
#model IEnumerable<RobotDog.Models.UserModel>
<!-- table of users -->
<div class="modify-form">
#Html.Action("ModifyUser")
</div>
<div class="create-user-form">
#Html.Action("CreateUser")
</div>
PartialView
#model RobotDog.Models.DynamicActionUserModel
#using(Html.BeginForm(Model.Action,"Admin", FormMethod.Post, new { #class = "ajax" })) {
<!-- Email -->
#Html.TextBoxFor(x => x.RegisterModel.Email, new { #class = inputSize, placeholder = "Email"})
<!-- UserName -->
#if(Model.Action == "ModifyUser") {
#Html.HiddenFor(x => x.RegisterModel.UserName)
<span class="input-xlarge uneditable-input">#Html.DisplayNameFor(x => x.RegisterModel.UserName)</span>
} else {
#Html.TextBoxFor(x => x.RegisterModel.UserName, new { #class = inputSize, placeholder = "User Name" })
}
<!-- Password -->
#if(Model.Action == "Createuser") {
#Html.PasswordFor(x => x.RegisterModel.Password, new { #class = inputSize, placeholder = "Password"})
}
<!-- Roles -->
#Html.ListBoxFor(x => x.RegisterModel.SelectedRoles, Model.RegisterModel.Roles)
<!-- Submit -->
<input type="submit" value="Submit" class="btn"/>
}
Try with ModelState.Remove("password") before calling ModelState.IsValid, but as suggested here if a property is not always required you should not mark it as required.
Have you looked at ModelState.IsValidField ?
Note: even as of MVC4 the documentation is backwards and should say :
Determines whether there are any ModelError objects that are associated with or prefixed with the specified key.
I've made a little extension method helper for this:
public static class ModelStateHelpers
{
public static bool IsValidFor<TModel, TProperty>(this TModel model,
System.Linq.Expressions.Expression<Func<TModel, TProperty>> expression,
ModelStateDictionary modelState)
{
string name = ExpressionHelper.GetExpressionText(expression);
return modelState.IsValidField(name);
}
}
Usage is simple:
bool billingValid = model.IsValidFor(x => x.CheckoutModel.BillingAddress, ModelState);

Categories