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();
}
Related
I'm working with ASP.NET Core MVC 5.0 in Visual Studio 2019. I had the scaffolder generate a controller with CRUD operations using Entity Framework Core. I see that there are two Edit() methods, the GET version of the method looks up the entity in the database and passes it to the Edit view. So far, everything makes sense. What I'm a bit confused about is the POST version of the Edit() method. This is what its signature and first statement look like:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Name")] Department department)
{
if (id != department.Id)
{
return NotFound();
}
// ... (Removed for brevity)
}
If the ID can be bound to the Department entity using model binding, why does it require the first int id as parameter? I have tried removing it along with the equality check and the edit still works. I have also noticed a hidden input type in my Edit view:
<input type="hidden" asp-for="Id" />
which I tried removing as well after removing the id parameter. The Edit() somehow still works. I would like to know the purpose of the ID parameter, the equality check id == department.Id in the Edit() method and the hidden input field in the Edit view.
I actually wanted to use a viewmodel instead of using my entity directly with BindAttribute but ran into this confusion.
My entity is called Department and it looks like this:
public class Department
{
public int Id { get; set; }
[Required(AllowEmptyStrings = false, ErrorMessage = "{0} is required")]
[StringLength(maximumLength: 100, MinimumLength = 1, ErrorMessage = "{0} should be between {2} and {1} characters")]
public string Name { get; set; }
public List<Course> Courses { get; set; }
}
For more context, here's what the GET version of Edit() looks like:
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var department = await _context.Departments.FindAsync(id);
if (department == null)
{
return NotFound();
}
return View(department);
}
Firstly,the hidden input in Edit View is to bind department.Id in Post Edit method. Because your department have Id property,so when you delete <input type="hidden" asp-for="Id" />,the Id in url will still bind to department.Id and int Id.If department.Id not exists,db will create a new one.Here is a demo:
Teacher:
public class Teacher
{
public int TeacherId { get; set; }
public string TeacherName { get; set; }
public int TeacherAge { get; set; }
}
Edit:
<div class="row">
<div class="col-md-4">
<form asp-action="Edit">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="TeacherName" class="control-label"></label>
<input asp-for="TeacherName" class="form-control" />
<span asp-validation-for="TeacherName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="TeacherAge" class="control-label"></label>
<input asp-for="TeacherAge" class="form-control" />
<span asp-validation-for="TeacherAge" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
Edit method:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("TeacherId,TeacherName,TeacherAge")] Teacher teacher)
{
if (ModelState.IsValid)
{
try
{
_context.Update(teacher);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!TeacherExists(teacher.TeacherId))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(teacher);
}
result:
Sometimes not each model have a property named Id as primary key,so the hidden input is necessary.
And the id is used to make sure the edited data Id is right.For example,if some people custom the page,and make the Id modifiable.Or some one change the value of hidden input.
Here is two demos:
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.
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.
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 these two classes:
State and Station :
public class State
{
public int Id { get; set; }
[Required]
public string Code { get; set; }
[Required]
public string Name { get; set; }
public virtual ICollection<Station> Stations { get; set; }
}
and
public class Station
{
public int Id { get; set; }
[Required]
public string Code { get; set; }
[Required]
public string Name { get; set; }
[Required]
public virtual State State { get; set; }
}
The design was "Code First" and I used migration to set up the database and it was like:
The station table saves StateId as foreign Key as it should
My StationsController.cs file has a Create Method in which I use ViewBag for listing the state names like this:
// GET: Stations/Create
public IActionResult Create()
{
ViewBag.StatesList = _context.States.ToList();
return View();
}
And finally my HttpPost Create method
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,Code,Name,State")] Station station)
{
if (ModelState.IsValid)
{
_context.Add(station);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(station);
}
My create.cshtml file has <select> tag like this:
<div class="form-group">
<label asp-for="State" class="control-label"></label>
<select asp-for="State" asp-items="#(new SelectList(ViewBag.StatesList, "Id", "Name","State"))" ></select>
<span asp-validation-for="State" class="text-danger"></span>
</div>
The Issue I am facing is that after clicking submit, my ModelState.isValid remains false and the State field is null as shown in this image:
The State Field is null
The Controller has been autogenerated and only two things I have changed: 1st is that I have added the ViewBag.StateList in the Create() method and second is that I have added a State field in Create([Bind("Id,Code,Name,State")].
Any help will be greatly appreciated and sorry for the long post..
regards
Ashutosh
I don't know how many times I have said in SO that you shouldn't send your entity model directly from database to the view, and listen to its postback. You should only generate a model (we call it ViewModel) that represents what the view needs.
Here is what I will do (I wrote everything by hand, not tested).
Create a view model for station creation view:
public class CreateStationViewModel
{
// You shouldn't have Station ID here as it's creation
[Required]
public string Code { get; set; }
[Required]
public string Name { get; set; }
[Display(Name = "State")]
public int SelectedStateId { get; set; }
public IDictionary<int, string> AvailableStates { get; set; }
}
Initialize this view model on the get method:
public IActionResult Create()
{
var vm = new CreateStationViewModel
{
// Construct a list of available states.
// We will use it as the dropdown options.
AvailableStates = _context.States
.ToDictionary(x => x.Id, x => $"{ x.Name }({ x.Code })")
};
return View(vm);
}
Build the form on the view:
#model CreateStationViewModel
#{
// You can define a variable here for the dictionary-to-selectListItem
// conversion.
// Or you can write an extension method on IDictionary for that purpose.
var availableStatesSelectListItems = Model.AvailableStates
.Select(x => new SelectListItem
{
Text = x.Value.ToString(),
Value = x.Key.ToString()
});
}
<form asp-area="" asp-controller="station" asp-action="create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Code" class="control-label"></label>
<input type="text" class="form-control" asp-for="Code" />
<span class="form-text" asp-validation-for="Code"></span>
</div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input type="text" class="form-control" asp-for="Name" />
<span class="form-text" asp-validation-for="Name"></span>
</div>
<div class="form-group">
<label asp-for="SelectedStateId" class="control-label"></label>
<select class="form-control" asp-for="SelectedStateId"
asp-items="availableStatesSelectListItems">
<option value="">- select -</option>
</select>
<span class="form-text" asp-validation-for="SelectedStateId"></span>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success">Create</button>
</div>
</form>
Listen to the view model on postback:
[HttpPost]
public IActionResult Create(CreateStationViewModel model)
{
if (ModelState.IsValid)
{
// You build the entity model from the view model
_context.Stations.Add(new Station
{
Code = model.Code,
Name = model.Name,
StateId = model.SelectedStateId
});
_context.SaveChanges();
return RedirectToAction("index");
}
// Rebuild the available states, or
// better, you can use ajax for the whole form (different topic)
model.AvailableStates = _context.States
.ToDictionary(x => x.Id, x => $"{ x.Name }({ x.Code })");
return View(model);
}
AFAIK you can't pass a State object to be used as the value of the <option> elements inside your <select>. Instead, you have to use a simpler datatype. In practice you need to use StateId as the value.
Add the property StateId to your Station class:
public class Station
{
public int Id { get; set; }
[Required]
public string Code { get; set; }
[Required]
public string Name { get; set; }
public virtual State State { get; set; }
[Required]
public int StateId { get; set; }
}
Also, your SelectList constructor is wrong. The fourth argument ("State" in your example) is supposed to define the default selected item. You don't need to define it here, so leave it out:
<select asp-for="StateId" asp-items="#(new SelectList(ViewBag.StatesList, "Id", "Name"))" ></select>
Now your <select> element should be able to select the correct Id and populate your StateId field. Note that in the [HttpPost] method, the Statewill still be null, but your database should be updated with the correct StateId.