I have working code for adding role to user. Now i want to replace text input by dropdownlist with available (all) roles in application. I now that there is html tag #Html.DropDownListFor so I created new ViewModel containing every data I need but now i'am in dead end. I try foreach and fill data for every item on list but then it doesn't work. Trying with #Htm.DropDownListFor have the same effect. Can someone can show me the right way how to do that ? Thanks !
Controller:
public async Task<IActionResult> AddRoleToUser(string id, string role)
{
var user = await _userManager.FindByIdAsync(id);
if (await _roleManager.RoleExistsAsync(role))
{
await _userManager.AddToRoleAsync(user, role);
}
return RedirectToAction("UserGroups", new { id });
}
ViewModel:
public class UserGroupsViewModel
{
public ApplicationUser ApplicationUser { get; set; }
public IList<string> RolesList { get; set; }
public IQueryable<ApplicationRole> AllRoles { get; set; }
}
View
#model AdminMVC.Models.Admin.UserGroupsViewModel
#using (Html.BeginForm("AddRoleToUser", "Admin", new { id = Model.ApplicationUser.Id }))
{
<div classs="input-group">
<p><b>Name new role for user:</b></p>
<input class="form-control form-control-sm" type="text" name="role" placeholder="Role name">
<span class="input-group-btn">
<input type="submit" value="Add Role" class="btn btn-primary btn-sm" />
</span>
</div>
}
My question has been identified as a possible duplicate of another question. But i saw that question and still cant do it.
Can I must change my IList to list containing two values id and string ? And add additional position in viewmodel to store the result?
Change your RolesList to a SelectList and add a role property:
public SelectList RolesList { get; set; }
public string Role{get;set;}
Build the SelectList with
model.RolesList = new SelectList(listOfRoles);
Then in your view
#Html.DropDownListFor(x => x.Role, Model.RolesList)
If everything is in order, your post should contain the role populated.
Related
I am trying to create an ASP.NET Core 3.1 MVC web app using identity. The app has users and admin. The admin can create new tasks and assign them to the users.
I get the list of users and tasks in 2 different dropdowns as follows:
View: Assign.cshtml
#model TaskManager2.ViewModels.AssignViewModel
#{
ViewData["Title"] = "Assign";
}
<h1>Assign</h1>
<h4>Assign</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Assign">
<div class="form-group">
#Html.DropDownListFor(t => t.TaskId,
Model.Tasks, "--Select task--")
</div>
<div class="form-group">
#Html.DropDownListFor(u => u.Id,
Model.Users, "--Select user--")
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
AdminController:
private readonly ApplicationDbContext _context;
public AdminController(ApplicationDbContext context)
{
_context = context;
}
public IActionResult Index()
{
return View();
}
public IActionResult Assign()
{
var users = (from u in _context.Users select new SelectListItem { Value = u.Id, Text = u.FirstName }).ToList();
var tasks = (from t in _context.Task select new SelectListItem { Value = t.TaskId.ToString(), Text = t.Description }).ToList();
var user = _context.Users.FirstOrDefault();
return View(new AssignViewModel { Users = users, Tasks = tasks });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Assign([Bind("TaskId, Id")] Assign assign)
{
if (ModelState.IsValid)
{
_context.Add(assign);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(assign);
}
ViewModel :
public class AssignViewModel
{
public IList<SelectListItem> Tasks;
public IList<SelectListItem> Users;
//public long SelectedUserId { get; set; }
//public long SelectedTaskId { get; set; }
//Added these lines instead
public Task TaskId { get; set; }
public IdentityUser Id { get; set; }
}
Model: Assign.cs
public partial class Assign
{
public long AssignId { get; set; }
public long TaskId { get; set; }
//[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Id { get; set; }
}
I got an error that is asked previously on stackoverflow: here and I tried the solution. I added the commented line on Assign.cs but the result that I get is not the one that I want. The idea is to save the Id of the user from AspNetUser and taskId from Task table in a third table called Assign. Here's how it looks like:
I am new to this, so I don't really understand how it works. I would really appreciate your help! Thank you!
Edit
I made the changes to the code above and also set Identity Specification to Yes for the primary key. It works and records the user Id and the corresponding taskId to the database. Now I'm trying to create something like:
#if ((bool)ViewData["HasError"]) //not working
{ <div class="alert alert-danger" role="alert">
Please select!
</div>
}
This will give an alert if the user doesn't select any of the options instead of throwing this error:
InvalidOperationException: The model item passed into the ViewDataDictionary is of type 'TaskManager2.Models.Assign', but this ViewDataDictionary instance requires a model item of type 'TaskManager2.ViewModels.AssignViewModel'.
In the http post line, you are sending Assign (entity model)
To the controller, so the model state would be done on Assign class,
I think you should be using AssignViewModel there, because all the interaction with views should happen with viewmodel, if your view model is valid then covert to actual entity.
The error message I get is this : DbUpdateConcurrencyException
I am using updating the IdentityRole table. I am using a ViewModel to display and capture data, then passing it back in, in my OnPost(). The data comes into my IdentityRole property as expected. I made sure the primary key is hidden in the form. I have also tried _context.Update(Role) in combination with SaveChanges() and i always get the same error. I also ensured i have the [BindProperty] on both my ViewModel and my IdentityRole properties.
Here is my Controller or code behind.
public IActionResult OnPost()
{
Role.Name = RoleViewModel.RoleName;
Role.NormalizedName = RoleViewModel.RoleName.ToUpper();
Role.ConcurrencyStamp = DateTime.Now.ToString();
if (!ModelState.IsValid)
{
return Page();
}
_context.Entry(Role).Property(x => x.Name).IsModified = true;
_context.Entry(Role).Property(x => x.NormalizedName).IsModified = true;
_context.Entry(Role).Property(x => x.ConcurrencyStamp).IsModified = true;
_context.SaveChanges();
return Redirect("/Admin/HomeRole");
}
My ViewModel
public class EditRoleViewModel
{
public EditRoleViewModel()
{
Users = new List<string>();
}
public string Id { get; set; }
[Required(ErrorMessage = "Role Name is required")]
public string RoleName { get; set; }
public List<string> Users { get; set; }
}
Here is the form
<form method="post" class="mt-3">
<input asp-for="RoleViewModel.Id" type="hidden" />
<input asp-for="RoleViewModel.Users" type="hidden" />
<div class="form-group row">
<label asp-for="RoleViewModel.RoleName" class="col-sm-2 col-form-label"></label>
<div class="col-sm-10">
<input asp-for="RoleViewModel.RoleName" class="form-control">
<span asp-validation-for="RoleViewModel.RoleName" class="text-danger"></span>
</div>
</div>
</form>
From what I see in your question, you need to pass the Id for Http Post. I understand that you are hiding the Id in the form but you need pass it to the controller and then to the Model to save. So, something like below,
public IActionResult OnPost(RoleViewModel model){......}
then assign the Id. If you are updating the row with no Id, then you will get this exception in DbContext.
For further reading in MSDN
Asp.Net controller snippets
Check these too. Try and let me know.
Here is the answer to my question
public async Task<IActionResult> OnPost(EditRoleViewModel roleViewModel)
{
var role = await roleManager.FindByIdAsync(roleViewModel.Id);
role.Name = roleViewModel.RoleName;
var result = await roleManager.UpdateAsync(role);
return Page();
}
I'm trying to basically have users "like" a post, which I call "voting" for a post in my application.
I have a vote class which records the userid, the postid (called a story), and the rest is boilerplate.
public class Vote
{
public int Id { get; set; }
public string VoterId { get; set; }
public virtual ApplicationUser Voter { get; set; }
public int StoryId { get; set; }
public Story Story { get; set; }
public DateTime CreatedAt { get; set; }
}
In the story model, I have a reference to a collection of votes which I theoretically will call a .Count() and print the number of "votes" to the view when it comes time to render the post details page.
public virtual ICollection<Vote> Votes { get; set; }
Inside the razor view, when a user is looking at a post(story), deciding whether or not to vote for it, I have this form. So if the user logged in isn't the author of the story, then they can vote for the story.
#if (!Model.IsStoryOwner)
{
<div class="row mt-1">
<div class="col-3">
#using (Html.BeginForm("New", "Vote"))
{
#Html.AntiForgeryToken()
#Html.HiddenFor(x => x.Story.Id, new { StoryId = Model.Story.Id })
<div class="btn-group">
<button type="submit" class="btn btn-info voteBtn" id="LikeBtn">
<i class="fa fa-heart mr-2"></i>
Vote
</button>
</div>
}
</div>
</div>
}
This hits the following controller:
//POST /vote/new
//FOR adding a vote to a story
[Authorize]
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult New(Vote vote)
{
var voterId = User.Identity.GetUserId();
var newVote = new Vote
{
VoterId = voterId,
StoryId = vote.StoryId,
CreatedAt = DateTime.Now
};
dbContext.Votes.Add(newVote);
dbContext.SaveChanges();
return View();
}
however the storyId is coming through as null even though I've verified that it's not on the client-side when I run the application.
Because your current view code will generate the HTML markup for a hidden input element with name attribute value Story.Id.
<input name="Story.Id" type="hidden" value="1">
When the form is submitted, model binder will not be able to map the value of that input to StoryId property of Vote object. For model binder to properly map the values, the input element name should match with the property name of the class you are using as the parameter of your action method.
You can create a hidden input with the name attribute value matching to your property name
#using (Html.BeginForm("New", "Vote"))
{
#Html.AntiForgeryToken()
<input type="hidden" name="StoryId" value="#Model.Story.Id" />
<div class="btn-group">
<button type="submit" class="btn btn-info voteBtn" id="LikeBtn">
<i class="fa fa-heart mr-2"></i>Vote
</button>
</div>
}
Or you can use the Html.Hidden helper method which generates the same markup as above.
#Html.Hidden("StoryId",Model.Story.Id)
You are using HiddenFor the Story.Id, not StoryId.
If your Model contains a value for StoryId, you can simply use
#Html.HiddenFor(x => x.StoryId)
I have an issue creating form which contains dynamic list with dynamic sub-list. after submit - nested list of the list item is empty.
How it works
I have a View which contains HtmlHelper extension wrapper
#Html.CollectionEditorFor(m => m.Problems, "Edit/_ProblemEditor", "/Opportunity/GetProblemEditor")
If simplify extension it looks like this
public static IHtmlString CollectionEditorFor<TModel, TItem>(
this HtmlHelper<TModel> html,
Func<TModel, IEnumerable<TItem>> collection,
string partialViewName,
string controllerActionPath)
{
var editorId = "CollectionEditor_" + Guid.NewGuid().ToString("N");
var output = new StringBuilder();
output.AppendLine(#"<ul id=""" + editorId + #""">");
var items = collection(html.ViewData.Model);
if (items != null)
{
foreach (var item in collection(html.ViewData.Model))
{
output.AppendLine(
viewDataDictionary != null
? html.Partial(partialViewName, item, viewDataDictionary).ToString()
: html.Partial(partialViewName, item).ToString());
}
}
output.AppendLine(#"</ul>");
/// Here some JS code to do adding and deleting
return new HtmlString(output.ToString());
}
Everything is simple here. I pass the model field, name of partial view to load and action which is used to load new partial view on adding element.
_ProblemEditor partial view looks like this:
#using BoxxStep.Web.Infrastructure.Extentions
#model BoxxStep.Web.Models.Opportunity.Details.ProblemViewModel
<li>
#using (Html.BeginCollectionItem("Problems"))
{
<div class="double">
<button value="Add" type="button" collection-button="buttonAddProblem" class="btn-icon add">
<i class="icon-plus"></i>
</button>
<button value="Delete" onclick="$(this).parent().parent().remove();" collection-button="deleteProblem" style="display: none;" class="btn-icon delete" type="button">
<i class="icon-minus"></i>
</button>
</div>
<div class="w-input">
<div class="col-lg-9 col-md-8 col-xs-6">
#Html.HiddenFor(m => m.Id)
#Html.TextBoxFor(m => m.ProblemText, new { placeholder = Model.GetDisplayName(m => m.ProblemText) })
<i class="icon-criteria" title="#Model.GetDisplayName(m => m.ProblemText)"></i>
</div>
</div>
<div class="col-md-12">
<div class="text-box">
#Html.CollectionEditorFor(m => m.Symptoms, "Edit/_ProblemSymptomEditor", "/Opportunity/GetProblemSymptomEditor")
</div>
</div>
}
</li>
Here I want to show nested list of Symptoms using the same wrapper and _ProblemSymptomEditor partial view which looks very similar:
#using BoxxStep.Web.Infrastructure.Extentions
#model BoxxStep.Web.Models.Opportunity.Details.ProblemSymptomViewModel
<li>
#using (Html.BeginCollectionItem("Problems.Symptoms"))
{
<div class="single">
<button value="Add" collection-button="buttonAddSymptom" type="button" class="btn-icon add">
<i class="icon-plus"></i>
</button>
<button value="Delete" onclick="$(this).parent().parent().remove();" collection-button="deleteSymptom" style="display: none;" class="btn-icon delete" type="button">
<i class="icon-minus"></i>
</button>
</div>
<div class="w-input">
#Html.HiddenFor(m => m.Id)
#Html.TextBoxFor(m => m.SymptomText, new { placeholder = Model.GetDisplayName(m => m.SymptomText) })
<i class="icon-motive" title="#Model.GetDisplayName(m => m.SymptomText)"></i>
</div>
}
</li>
Actions which I use to add new items very simple and looks like this:
To load for parent list:
[HttpGet]
public ActionResult GetProblemEditor()
{
return this.PartialView(
"Edit/_ProblemEditor",
new ProblemViewModel
{
Symptoms = new List<ProblemSymptomViewModel> { new ProblemSymptomViewModel() }
});
}
And to load nested list item:
[HttpGet]
public ActionResult GetProblemSymptomEditor()
{
return this.PartialView("Edit/_ProblemSymptomEditor", new ProblemSymptomViewModel());
}
Parent simplified View Model which contains list of Problems
public class OpportunityEditViewModel
{
public OpportunityEditViewModel()
{
this.Problems = new List<ProblemViewModel>();
}
[DisplayName("Company Name")]
public string CompanyName { get; set; }
public int Id { get; set; }
[DisplayName("Problems")]
public List<ProblemViewModel> Problems { get; set; }
[DisplayName("Town / City")]
public string TownCity { get; set; }
}
ProblemViewModel view model is here. It contains list of Symptoms:
public class ProblemViewModel
{
public ProblemViewModel()
{
this.Symptoms = new List<ProblemSymptomViewModel>();
}
public int Id { get; set; }
[DisplayName("Problem")]
public string ProblemText { get; set; }
[DisplayName("Symptoms")]
public List<ProblemSymptomViewModel> Symptoms { get; set; }
}
And finally view model of Symptom is fairly simple:
public class ProblemSymptomViewModel
{
public int Id { get; set; }
[DisplayName("Symptoms")]
public string SymptomText { get; set; }
}
Form is submitted to next action:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<JsonResult> SaveCompany(OpportunityEditViewModel model)
{
if (!this.ModelState.IsValid)
{
this.AddValidateMessages();
return this.Json(new { success = false }, JsonRequestBehavior.AllowGet);
}
/// Some Business logic here
return
this.Json(
new
{
success = result.Succeeded,
companyId = result.Entity.CompanyID,
opportunityId = result.Entity.Id
},
JsonRequestBehavior.AllowGet);
}
And on submit - everything is cool with Problems. List of problems reflects correct amount of items on form, correct data, everything is ok but nested list of Symptoms is completely empty. And it doesn't meter if I add it dynamically or I prepare list of elements on load.
Rendered HTML looks like this:
Can somebody help me to solve the issue? I've spent week trying to solve the issue and without result. It looks like only me want to have list inside list :) With one level list everything is ok and works. But if have 2 levels lists - it doesn't work at all. Strange.
Thank you a lot for any help
I have a problem while passing an object with HttpPost...
Once the form is submitted, the model is set "null" on the controller side, and I don't know where is the issue..
Here is my controller :
public ActionResult AddUser(int id = 0)
{
Group group = db.Groups.Find(id);
List<User> finalList = db.Users.ToList() ;
return View(new AddUserTemplate()
{
group = group,
users = finalList
});
//Everything is fine here, the object is greatly submitted to the view
}
[HttpPost]
public ActionResult AddUser(AddUserTemplate addusertemplate)
{
//Everytime we get in, "addusertemplate" is NULL
if (ModelState.IsValid)
{
//the model is null
}
return View(addusertemplate);
}
Here is AddUserTemplate.cs :
public class AddUserTemplate
{
public Group group { get; set; }
public User selectedUser { get; set; }
public ICollection<User> users { get; set; }
}
Here is the form which return a null value to the controller (note that the dropdown list is greatly populated with the good values) :
#using (Html.BeginForm()) {
<fieldset>
<legend>Add an user</legend>
#Html.HiddenFor(model => model.group)
#Html.HiddenFor(model => model.users)
<div class="editor-field">
//Here, we select an user from Model.users list
#Html.DropDownListFor(model => model.selectedUser, new SelectList(Model.users))
</div>
<p>
<input type="submit" value="Add" />
</p>
</fieldset>
}
Thanks a lot for your help
I tried your code and in my case the addusertemplate model was not null, but its properties were all null.
That's because of a few model binding issues: Html.HiddenFor and Html.DropDownListFor do not work with complex types (such as Group or User) (at least that's how it is by default).
Also, Html.HiddenFor cannot handle collections.
Here's how to solve these issues:
instead of #Html.HiddenFor(model => model.group) there should be one #Html.HiddenFor for each property of the group that you need bound
instead of #Html.HiddenFor(model => model.users) you need to iterate through the list of users and for each object add #Html.HiddenFor for each property of the user that you need bound
instead of #Html.DropDownListFor(model => model.selectedUser [...], create a property like int SelectedUserId {get;set;} and use that in the DropDownList (as it cannot handle complex types).
Here's the code that works:
1. The User and Group classes, as I imagined them to be:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Group
{
public int Id { get; set; }
public string Name { get; set; }
}
2. The adjusted AddUserTemplate class:
public class AddUserTemplate
{
public Group Group { get; set; }
public IList<User> Users { get; set; }
public int SelectedUserId { get; set; }
public User SelectedUser
{
get { return Users.Single(u => u.Id == SelectedUserId); }
}
}
The adjustments:
Users was changed from ICollection to IList, because we'll need to access elements by their indexes (see the view code)
added SelectedUserId property, that will be used in the DropDownList
the SelectedUser is not a readonly property, that returns the currently selected User.
3. The adjusted code for the view:
#using (Html.BeginForm())
{
<fieldset>
<legend>Add an user</legend>
#*Hidden elements for the group object*#
#Html.HiddenFor(model => model.Group.Id)
#Html.HiddenFor(model => model.Group.Name)
#*Hidden elements for each user object in the users IList*#
#for (var i = 0; i < Model.Users.Count; i++)
{
#Html.HiddenFor(m => m.Users[i].Id)
#Html.HiddenFor(m => m.Users[i].Name)
}
<div class="editor-field">
#*Here, we select an user from Model.users list*#
#Html.DropDownListFor(model => model.SelectedUserId, new SelectList(Model.Users, "Id", "Name"))
</div>
<p>
<input type="submit" value="Add" />
</p>
</fieldset>
}
Another option that does not require a bunch of hidden fields is to simply specify that you want the model passed to the controller. I think this is much cleaner.
#using(Html. BeginForm("action","controller", Model, FormMethod.Post)){
...
}