Populate mvc dropdownlist based on user role - c#

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.

Related

Validation error with readonly input attribute and [Required] data Annotation

As we know when you create an ASP.NET Core appp using Individual User Authentication project template, it creates a default ResetPassword.cshtml view. In that View I need to set logged in user name input tag as readonly. But doing so is throwing the following validation error. If I don't make it readonly the below screen successfully allows user to change password.
Question: Why the following validation error on form submit - when the UserName input tag is set to readonly? I know that if the input tag is disabled then form submit does not submit the input tag's value (also explained by #AdamBellaire here). It seems [Required] annotation in public string UserName { get; set; } is somehow conflicting with readonly attribute of input tag.
public class ResetPasswordViewModel
{
[Required]
public string UserName { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.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; }
public string Code { get; set; }
}
UPDATE
ResetPassword.cshtml: [It's a default View created by VS2017. Only change is that I added readonly attribute in input tag below]
#model ResetPasswordViewModel
...
<input asp-for="#User.Identity.Name" class="form-control" readonly />
...
In this situation, it really makes no sense to expect #User.Identity.Name in the view to bind to UserName in the view model.
It would seem that the code the IDE generated is wrong. Maybe a messed up scaffolding template somewhere, who knows.
You need to change your asp-for to equal UserName. See below.
<input asp-for="UserName" class="form-control" readonly />
Glad this helped!

View with Multiple Models and Posting only one

The situation is that I have a complex model with a lots of data to view and aside to it, control panel with for example password change.
One big model with another property model which will be submitted.
The information inside the big model requires loading and is not required upon POSTing
The Model
public class ProfileModel {
// This is the submitted model:
public PasswordChangeModel Password = new PasswordChangeModel();
// Personal Info
public string Name {get; set;}
public string LastName {get; set;}
// 15~ more fields
}
The password model w/ validation
public class PasswordChangeModel {
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "OldPassword")]
public string OldPassword { 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 = "Repeat password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string RepeatPassword { get; set; }
}
Controller Catching-Action
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult ChangePassword(PasswordChangeModel model) {
if (!ModelState.IsValid) //validate the model
return View(model);
//do stuff ...
return Index();
}
The Html to generate the form
<form asp-controller="Profile" asp-action="ChangePassword" asp-antiforgery="true">
<div asp-validation-summary="ValidationSummary.ModelOnly" class="text-danger"></div>
<label asp-for="Password.OldPassword">Old Password</label>
<input asp-for="Password.OldPassword"/>
<label asp-for="Password.Password">New Password</label>
<input asp-for="Password.Password"/>
<label asp-for="Password.RepeatPassword">New Password Repeat</label>
<input asp-for="Password.RepeatPassword"/>
<input type="submit" class="btn" name="submit" value="Change"/>
</form>
The Question
Now after reviewing the code, my question is - is it possible to submit it that way, if not whats the most convenient and clean way to do it.
Note: I always can just include 3 fields inside the model ProfileModel of the password changing but A-It's ugly and B-It still makes the entire ProfileModel data to load.
I would say that the cleanest way to do this is to have a separate update password view. This or switching to a ajax post so you can post without reloading the page. If you can't make a model that could do a roundtrip to the server without repopulating it then don't do standard form posting. It can be done but when I've seen it usually there are subtle errors when rerendering the page on validation error.
It's just easy to shoot yourself in the foot.
This is what I've ended up doing.
Worked just fine.
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult ChangePassword([Bind(Prefix = "Password.OldPassword")]string Password_OldPassword,
[Bind(Prefix = "Password.Password")] string Password_Password,
[Bind(Prefix = "Password.RepeatPassword")] string Password_RepeatPassword) {
//Change the password
}
Bind attribute redirected the value of Password.OldPassword to Password_OldPassword.

Bind Model to Radio Buttons on POST

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.

bypassing model validation in controller?

I have created an ADO.NET model of my database.
Created a new controller with CRUD (entity framework and using the ADO.NET entity model I created).
In my database I have a simple Users table. The Password row in the table will hold the users passwords encrypted with SimpleCrypto (PBKDF2).
In my ADO.NET Users.cs model I have added following validation:
[Required]
[DataType(DataType.Password)]
[StringLength(20, MinimumLength = 6)]
[Display(Name = "Password")]
public string Password { get; set; }
That works with jQuery in the browser with validation.
But in my controller I am encrypting the Password, and then the Password string will be way more than 20 chars in lenght.
var crypto = new SimpleCrypto.PBKDF2();
var encryptedPass = crypto.Compute(user.Password);
user.Password = encryptedPass;
user.PasswordSalt = crypto.Salt;
_db.Users.Add(user);
_db.SaveChanges();
And this gives me and "Validation failed for one or more entities."-error.
I can copy the user over to a "var newUser" and then set all the properties there, but isn't there a easier way to bypass the model validation in this case?
EDIT: If I remove the validation of the Password prop in the model then everything works. So it is the validation that gives me the error because I alter the Password from 6-20 length chars to +100 lengt chars because of the encryption in the controller.
EDIT: Complete controller section inserted to this question.
[HttpPost]
public ActionResult Create(Users user)
{
if (!ModelState.IsValid)
{
return View();
}
if (_db.Users.FirstOrDefault(u => u.Email == user.Email) != null)
{
ModelState.AddModelError("", "User already exists in database!");
return View();
}
var crypto = new SimpleCrypto.PBKDF2();
var encryptedPass = crypto.Compute(user.Password);
user.Password = encryptedPass;
user.PasswordSalt = crypto.Salt;
_db.Users.Add(user);
_db.SaveChanges();
return RedirectToAction("Index", "User");
}
This is where ViewModels enter the game. You should create a model which you pass to the view and map that back to the domain model later.
The ViewModel:
public class RegisterModel
{
[Required]
public string UserName { get; set; }
[Required]
[DataType(DataType.Password)]
[StringLength(20, MinimumLength = 6)]
[Display(Name = "Password")]
public string Password { get; set; }
}
The domain model (your current User model):
public class User
{
// other properties..
[Required]
public string Password { get; set; }
}
You can use these models in your controller like this:
The GET action:
public ActionResult Register()
{
var registerModel = new RegisterModel();
return View(registerModel)
}
With a view like this:
#model RegisterModel
#Html.LabelFor(model => model.UserName)
#Html.TextBoxFor(model => model.UserName)
#Html.ValidationMessageFor(model => model.UserName)
#Html.LabelFor(model => model.Password)
#Html.PasswordFor(model => model.Password)
#Html.ValidationMessageFor(model => model.Password)
And the POST action:
[HttpPost]
public ActionResult Register(RegisterModel registerModel)
{
// Map RegisterModel to a User model.
var user = new User
{
UserName = registerModel.UserName,
Password = registerModel.Password // Do the hasing here for example.
};
db.Users.Add(user);
db.SaveChanges();
}
You say your password encryption is occurring in the controller. In this case shouldn't you be encrypting after validation? For example:
public ActionResult SomeControllerAction(UserViewModel user)
{
if (!ModelState.IsValid)
{
// at this point the human readable (and assuming < 20 length) password
// would be getting validated
return View(user);
}
// now when you're writing the record to the DB, encrypt the password
var crypto = new SimpleCrypto.PBKDF2();
var encryptedPass = crypto.Compute(user.Password);
user.Password = encryptedPass;
user.PasswordSalt = crypto.Salt;
_db.Users.Add(user);
_db.SaveChanges();
// return or redirect to whatever route you need
}
If you wanted to specifically control your validation, then try implementing IValidatableObject on your view model class and perform validation here, instead of via attributes. For example:
public class UserViewModel : IValidatableObject
{
public string Username { get; set; }
public string Password { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// validate the unencrypted password's length to be < 20
if (this.Password.Length > 20)
{
yield return new ValidationResult("Password too long!");
}
}
}
If i understand correctly you have a database table with a password field.
According to your model this password field is 20 characters long
[StringLength(20, MinimumLength = 6)]
And you want to insert a value greater then 20 characters.
If entity framework did not stop you then you would get a database error.(Entity framework doesn't know there is a mismatch between your data model and the database and it doesn't wanna take the risk of pushing the insert through)
I assume however that you ment to specify a clientSide validation rule on your viewmodel and not a length constraint on the database.
I hope you see why this is a confusing setup.
My advice would be to either split your viewModel and model up so you can post a viewModel with unencrypted password of maxlength 20 that you can convert to a model password with length 100.
If you find that too much hassle you could create an unmapped password property which you set from html when you post it and you convert it to the password property in your controller.
Your class could look like this :
public class RegisterModel
{
[Required]
public string UserName { get; set; }
[Required]
[NotMapped]
[StringLength(20, MinimumLength = 6)]
[Display(Name = "Password")]
public string PlainTextPassword { get; set; }
[Required]
[StringLength(300)]//This is optional
[DataType(DataType.Password)]
public string Password { get; set; }
}

#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.

Categories