I'm working with ASP.NET MVC 4 and Entity Framework and I was searching for some way to make many to many relation and checkboxes from my db for a Create/Edit controller and view, I have found the answer with #Slauma answer for Create in MVC 4 - Many-to-Many relation and checkboxes but, I'd really like to see how this extends to Edit and Delete functionality as well like some other partners in this solution. Could someone please show how I would populate the ClassificationSelectViewModel in the Edit controller method to get both the "checked" and "unchecked" values? this is a Matt Flowers question that will solve mine too.
The following is a continuation of this answer that describes Create GET and POST actions for a model with many-to-many relationship between entities Subscription and Company. Here is the procedure for the Edit actions how I would do it (except that I probably wouldn't put all the EF code into the controller actions but extract it into extension and service methods):
The CompanySelectViewModel remains unchanged:
public class CompanySelectViewModel
{
public int CompanyId { get; set; }
public string Name { get; set; }
public bool IsSelected { get; set; }
}
The SubscriptionEditViewModel is the SubscriptionCreateViewModel plus the Subscription's key property:
public class SubscriptionEditViewModel
{
public int Id { get; set; }
public int Amount { get; set; }
public IEnumerable<CompanySelectViewModel> Companies { get; set; }
}
The GET action could look like this:
public ActionResult Edit(int id)
{
// Load the subscription with the requested id from the DB
// together with its current related companies (only their Ids)
var data = _context.Subscriptions
.Where(s => s.SubscriptionId == id)
.Select(s => new
{
ViewModel = new SubscriptionEditViewModel
{
Id = s.SubscriptionId
Amount = s.Amount
},
CompanyIds = s.Companies.Select(c => c.CompanyId)
})
.SingleOrDefault();
if (data == null)
return HttpNotFound();
// Load all companies from the DB
data.ViewModel.Companies = _context.Companies
.Select(c => new CompanySelectViewModel
{
CompanyId = c.CompanyId,
Name = c.Name
})
.ToList();
// Set IsSelected flag: true (= checkbox checked) if the company
// is already related with the subscription; false, if not
foreach (var c in data.ViewModel.Companies)
c.IsSelected = data.CompanyIds.Contains(c.CompanyId);
return View(data.ViewModel);
}
The Edit view is the Create view plus a hidden field for the Subscription's key property Id:
#model SubscriptionEditViewModel
#using (Html.BeginForm()) {
#Html.HiddenFor(model => model.Id)
#Html.EditorFor(model => model.Amount)
#Html.EditorFor(model => model.Companies)
<input type="submit" value="Save changes" />
#Html.ActionLink("Cancel", "Index")
}
The editor template to select a company remains unchanged:
#model CompanySelectViewModel
#Html.HiddenFor(model => model.CompanyId)
#Html.HiddenFor(model => model.Name)
#Html.LabelFor(model => model.IsSelected, Model.Name)
#Html.EditorFor(model => model.IsSelected)
And the POST action could be like this:
[HttpPost]
public ActionResult Edit(SubscriptionEditViewModel viewModel)
{
if (ModelState.IsValid)
{
var subscription = _context.Subscriptions.Include(s => s.Companies)
.SingleOrDefault(s => s.SubscriptionId == viewModel.Id);
if (subscription != null)
{
// Update scalar properties like "Amount"
subscription.Amount = viewModel.Amount;
// or more generic for multiple scalar properties
// _context.Entry(subscription).CurrentValues.SetValues(viewModel);
// But this will work only if you use the same key property name
// in ViewModel and entity
foreach (var company in viewModel.Companies)
{
if (company.IsSelected)
{
if (!subscription.Companies.Any(
c => c.CompanyId == company.CompanyId))
{
// if company is selected but not yet
// related in DB, add relationship
var addedCompany = new Company
{ CompanyId = company.CompanyId };
_context.Companies.Attach(addedCompany);
subscription.Companies.Add(addedCompany);
}
}
else
{
var removedCompany = subscription.Companies
.SingleOrDefault(c => c.CompanyId == company.CompanyId);
if (removedCompany != null)
// if company is not selected but currently
// related in DB, remove relationship
subscription.Companies.Remove(removedCompany);
}
}
_context.SaveChanges();
}
return RedirectToAction("Index");
}
return View(viewModel);
}
The Delete actions are less difficult. In the GET action you could load a few subscription properties to display on the delete confirmation view:
public ActionResult Delete(int id)
{
// Load subscription with given id from DB
// and populate a `SubscriptionDeleteViewModel`.
// It does not need to contain the related companies
return View(viewModel);
}
And in the POST action you load the entity and delete it then. There is no need to include the companies because in a many-to-many relationship (usually) cascading delete on the link table is enabled so that the database will take care of deleting the link entries together with the parent Subscription:
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirm(int id)
{
var subscription = _context.Subscriptions.Find(id);
if (subscription != null)
_context.Subscriptions.Remove(subscription);
return RedirectToAction("Index");
}
Related
I am using an ASP.NET MVC Razor view and Entity Framework to do CRUD operations. I am using a ViewModel to save changes to the model. I also have file uploads as part of this. The Create function works fine, but the Edit is where I keep getting the error
"Store update, insert, or delete statement affected an unexpected number of rows (0)."
And I am not sure where the trouble is.
My code is as follows:
View model:
public class ModelNameVM
{
public int? PinNumbers { get; set; }
public HttpPostedFileBase SerialAttachment { get; set; }
public HttpPostedFileBase CountryAttachment { get; set; }
public HttpPostedFileBase OtherAttachment { get; set; }
}
Edit.cshtml:
#model ModelName
....
#using (Html.BeginForm())
{
#Html.HiddenFor(m => m.Id)
#Html.TextBoxFor(m => m.PinNumbers)
#Html.TextBoxFor(m => m.SerialAttachment, new { type = "file" })
#Html.TextBoxFor(m => m.CountryAttachment , new { type = "file" })
...
Controller:
//GET Edit
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
ModelName data= db.entityname.Find(id); //
if (data== null)
{
return HttpNotFound();
}
return View(data);
}
//POST Edit
public ActionResult Edit(ModelNameVM model)
{
if (!ModelState.IsValid)
{
return View(model);
}
ModelName data = new ModelName
{
PinNumbers = model.PinNumbers
};
if (model.SerialAttachment != null && model.SerialAttachment.ContentLength > 0)
{
string fileName = Path.GetFileName(model.SerialAttachment.FileName);
.....
model.SerialAttachment.SaveAs(path);
data.SerialAttachment = path
}
.... // repeat for CountryAttachment and OtherAttachment
db.Entry(data).State = EntityState.Modified;
db.SaveChanges();
return View("Index", db.entityname.ToList());
}
It doesn't look like in your Edit Post Action you are retrieving the existing model/entity. You are passing back to the controller your ViewModel and not the actual model, so it seems like you are just creating a new one, so there would be nothing to update.
Instead of creating the new ModelName:
ModelName data = new ModelName
{
PinNumbers = model.PinNumbers
};
Change it to(you will need to add the id to the viewModel):
var data = db.entityname.Find(model.id);
if (data!= null)
{
data.PinNumbers = model.PinNumbers
}
else { return HttpNotFound(); }
Or since in your Edit Get Method you are returning the Model to the View, you could change your method signature from:
public ActionResult Edit(ModelNameVM model)
To:
public ActionResult Edit(ModelName model)
Im converting ASP.NET Identity 2.0: Implementing Group-Based Permissions Management to ASP.NET Core , all is good, but i cannot get the role from groups
heres the code :
ApplicationGroup Entity
public sealed class ApplicationGroup
{
[Key]
public string Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public ICollection<ApplicationGroupRole> ApplicationGroupRoles { get; set; }
public ICollection<ApplicationGroupUser> ApplicationGroupUsers { get; set; }
public ApplicationGroup()
{
Id = Guid.NewGuid().ToString();
ApplicationGroupRoles = new List<ApplicationGroupRole>();
ApplicationGroupUsers = new List<ApplicationGroupUser>();
}
}
ApplicationGroupRole Entity
public class ApplicationGroupRole
{
public string ApplicationRoleId { get; set; }
public string ApplicationGroupId { get; set; }
[ForeignKey("ApplicationGroupId")]
public ApplicationGroup ApplicationGroup { get; set; }
}
ApplicationGroupUser Entity
public class ApplicationGroupUser
{
public string ApplicationUserId { get; set; }
public string ApplicationGroupId { get; set; }
[ForeignKey("ApplicationGroupId")]
public ApplicationGroup ApplicationGroup { get; set; }
}
DbContext OnModelCreating :
modelBuilder.Entity<ApplicationGroup>()
.HasMany(u => u.ApplicationGroupUsers);
modelBuilder.Entity<ApplicationGroupUser>()
.HasKey(r => new {r.ApplicationUserId, r.ApplicationGroupId});
modelBuilder.Entity<ApplicationGroup>()
.HasMany(g => g.ApplicationGroupRoles);
modelBuilder.Entity<ApplicationGroupRole>()
.HasKey(gr => new { gr.ApplicationRoleId, gr.ApplicationGroupId});
modelBuilder.Entity<ApplicationGroupUser>().ToTable("ApplicationUserGroups");
modelBuilder.Entity<ApplicationGroupRole>().ToTable("ApplicationRoleGroups");
modelBuilder.Entity<ApplicationGroup>().ToTable("ApplicationGroups");
Controller :
public ActionResult Details(string id)
{
if (id == null)
{
return new StatusCodeResult(400);
}
ApplicationGroup applicationgroup = _groupManager.Groups.FirstOrDefault(g => g.Id == id);
if (applicationgroup == null)
{
return new NotFoundResult();
}
var groupRoles = _groupManager.GetGroupRoles(applicationgroup.Id);
var RoleNames = groupRoles.Select(p => p.Name).ToArray();
ViewBag.RolesList = RoleNames;
ViewBag.RolesCount = RoleNames.Count();
return View(applicationgroup);
}
View :
#model ApplicationGroup
#{
ViewBag.Title = "Details";
}
<h2>Details</h2>
<div>
<h4>ApplicationGroup</h4>
<hr />
<dl class="dl-horizontal">
<dt>
#Html.DisplayNameFor(model => model.Name)
</dt>
<dd>
#Html.DisplayFor(model => model.Name)
</dd>
<dt>
#Html.DisplayNameFor(model => model.Description)
</dt>
<dd>
#Html.DisplayFor(model => model.Description)
</dd>
</dl>
</div>
<h4>List of permissions granted this group</h4>
#if (ViewBag.PermissionsCount == 0)
{
<hr />
<p>No users found in this role.</p>
}
<table class="table">
#foreach (var item in ViewBag.RolesList)
{
<tr>
<td>
#item
</td>
</tr>
}
</table>
<p>
#Html.ActionLink("Edit", "Edit", new { id = Model.Id }) |
#Html.ActionLink("Back to List", "Index")
</p>
The result is when i got to the view, theres no roles, please help fix to fix this, where do i got wrong ?
Screen Shoot
Update for solution:
Need to change the controller to this :
public ActionResult Details(string id)
{
if (id == null)
{
return new StatusCodeResult(400);
}
var applicationgroup = _groupManager.Groups
.Where(g => g.Id == id)
.Select(g => new ApplicationGroup()
{
ApplicationGroupRoles = g.ApplicationGroupRoles
}).FirstOrDefault();
if (applicationgroup == null)
{
return new NotFoundResult();
}
var groupRoles = _groupManager.GetGroupRoles(applicationgroup.Id);
var RoleNames = groupRoles.Select(p => p.Name).ToArray();
ViewBag.RolesList = RoleNames;
ViewBag.RolesCount = RoleNames.Count();
return View(applicationgroup);
}
and need to change some query in other class to :
var grp = _db.ApplicationGroups
.Where(y => y.Id == groupId)
.Select(g => new ApplicationGroup()
{
ApplicationGroupRoles = g.ApplicationGroupRoles
})
.FirstOrDefault();
In Identity 2 I did something similar to the group permissions project you referenced. Except I named them Profiles instead of Groups. I started to do the same thing in Identity Core but then realized there is a new Identity table named UserRoleClaim that I can use instead. With this new table I accomplished the same thing without having to make modifications to the Identity Core database schema. Also, I don't have to write my own EF code. Everything can be done with the built-in methods in the UserManager and RoleManager classes.
You can equate a group to a role now. Each role can have many claims and many users can belong to a role. The key to authorizing users is the claims that are related to the role(s) the user belongs to. You cannot use the claims directly in the authorize attribute (that would be messy). You have to associate claims to a policy. Policies are the awesomeness in Identity Core! They make it so you can organize your claims into profiles. Then you only have to add a profile to your Authorize attribute. Which is much better than adding a list of roles and/or claims like you had to in Identity 2.
The profiles are created with the services.AddAuthorization() method in the ConfigureServices() method of class Startup.
services.AddAuthorization(options =>
{
options.AddPolicy("Product", policy => policy.RequireClaim("Product"));
options.AddPolicy("ProductEdit", policy => policy.RequireClaim("Product", "Edit"));
});
You can use these policies in an Authorize attribute.
[Authorize(Policy = "Product")]
public class ProductController : Controller
{
[HttpGet]
public IActionResult Index()
{
return View();
}
[HttpGet]
[Authorize(Policy = "ProductEdit")]
public IActionResult Edit()
{
return View();
}
}
I would read up on Authorization in Identity Core before making any design decisions. The documentation is actually pretty good and easy to read. I suggest at least reading these articles.
Claims-Based Authorization
Policy-Based Authorization
I've described the basics of claims and policy based authorization in Identity Core. But follow the rest of the articles in the documentation I linked to above and you will see that there are many more options available to you.
Step 1 is to check your database directly, and make sure that you have some ApplicationGroupRole rows that have an ApplicationGroupId that corresponds to the Id guid that can be seen in that screenshot.
If you do, then your problem might be that you're not including the ApplicationGroupRoles in your query.
To include your ApplicationGroupRoles, you can use the Include Extension method. Make sure you have this using statement.
using System.Data.Entity
And then do your query like this
ApplicationGroup applicationgroup = _groupManager.Groups
.Include(g => g.ApplicationGroupRoles)
.FirstOrDefault(g => g.Id == id);
Just a note: You don't need Include statements, if you project your result onto a view model before you return from the db. I.E.
var VM = _groupManager.Groups
.Where(g => g.Id == id)
.Select(g => new MyGroupViewModel()
{
Roles = g.ApplicationGroupRoles
})
.FirstOrDefault();
Using EntityFramework 6, I would like to update Customer in the following scenario:
public class Customer
{
public int Id {get; set;}
// 100 more scalar properties
public virtual IList<Consultant> Consultants {get;set;}
}
public class Consultant
{
public int Id {get; set;}
public virtual IList<Customer> Customers{get;set;}
}
This is my ViewModel for the edit view:
public class CustomerViewModel
{
public string[] SelectedConsultants { get; set; }
public IEnumerable<Consultants> AllConsultants{ get; set; }
public Customer Customer{ get; set; }
}
This is my Edit-ActionMethod:
[HttpPost]
public ActionResult Edit(CustomerViewModel vm)
{
if (ModelState.IsValid)
{
// update the scalar properties on the customer
var updatedCustomer = vm.Customer;
_db.Customers.Attach(updatedCustomer );
_db.Entry(updatedCustomer ).State = EntityState.Modified;
_db.SaveChanges();
// update the navigational property [Consultants] on the customer
var customer = _db.Customers
.Include(i => i.Customers)
.Single(i => i.Id == vm.Customer.Id);
Customer.Consultants.Clear();
_db.Consultants.Where(x => vm.SelectedConsultants
.Contains(x.Id)).ToList()
.ForEach(x => customer.Consultants.Add(x));
_db.Entry(customer).State = EntityState.Modified;
_db.SaveChanges();
return RedirectToAction("Index");
}
return View(vm);
}
This works and both scalar properties and consultants are updateable from the edit view. However, I am doing two _db.SaveChanges(); in my controller. Is there a less complex way to update Customer? Because Customer has many properties, I'd preferably not do a manual matching of all parameters on Customer and vm.Customer.
I have found the following resources:
asp.net official seems overly complicated (see section Adding
Course Assignments to the Instructor Edit Page) plus would require
me to explicitly write all parameters of Customer)
this popular thread on SO. Method 3 looks like what I need but I could not get the navigational property updated.
I don't think it's necessary to call the SaveChanges twice.
Have you tried something like this:
var customer = _db.Customers
.Where(c => c.Id== vm.Customer.Id)
.Include(c => c.Consultants)
.SingleOrDefault();
customer.Consultants = _db.Consultants
.Where(x => vm.SelectedConsultants.Contains(x.Id)).ToList();
_db.SaveChanges();
Edit:
Ok, not sure if this will work, but you can try using Automapper:
var customer = Mapper.Map<Customer>(vm.Customer);
_db.Entry(customer).State = EntityState.Modified;
customer.Consultants = _db.Consultants.Where(x => vm.SelectedConsultants.Contains(x.Id)).ToList();
_db.SaveChanges();
So I fixed the issue from a previous question I asked here, now I am getting what I was looking for but when I hit save it does nothing? No error or postback and nothing saved to the database? The button doesn't do anything in my view?
I have a many to many relationship with Post and Tag in my EF Model. I am trying to assign a post to a tag. I was following this tutorial. However the button does nothing when I click save.
PostController:
public ActionResult EditPostTag(int id = 12) // hardcoded the post for now
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var postsViewModel = new PostsViewModel
{
posts = db.Posts.Include(i => i.Tags).First(i => i.Id == id),
};
if (postsViewModel.posts == null)
return HttpNotFound();
var tagsList = db.Tags.ToList();
postsViewModel.Tags = tagsList.Select(o => new SelectListItem
{
Text = o.Name,
Value = o.Id.ToString()
});
ViewBag.UserID =
new SelectList(db.BlogUsers, "UserID", "Email", postsViewModel.posts.Id);
return View(postsViewModel);
}
Your view does not have a form tag
#model MyBlogger.ViewModel.PostsViewModel
#{
ViewBag.Title = "EditPostTag";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>EditPostTag</h2>
#using (Html.BeginForm()) // add this
{
....
}
Edit (further to comments and some misunderstanding by OP)
Using a view model as you have done is always good practice however you are not taking advantage of it by continuing to use ViewBag and using it to hold a data model instead of including just the properties you need to the view. I recommend it be
public class PostViewModel // its for a single post - not plural?
{
public int ID { get; set; }
[Required(ErrorMessage = "Please enter a title")]
public string Title { get; set; }
[Display(Name = "Tags")] // plus [Required] is at least one tag must be selected
public List<int> SelectedTags { get; set; }
public SelectList TagsList { get; set; }
// Its unclear if you really need the following 2 properties (see notes)
[Display(Name = "User")]
[Required(ErrorMessage = "Please select a user")]
public int UserID { get; set; }
public SelectList UserList { get; set; }
}
Side note: Its a bit unclear why you are allowing the user to select another user to be associated with the Post object. I suspect that when you save the Post you should be just assigning the current user in the controllers POST method
Your controller methods would then be (assume this is PostController)
public ActionResult Edit(int id)
{
Post post = db.Posts.Include(i => i.Tags).FirstOrDefault(i => i.Id == id); // First() will throw an exception is the item is not found
if (post == null) { return HttpNotFound(); }
PostViewModel model = new PostViewModel()
{
ID = post.ID,
Title = post.Title,
SelectedTags = post.Tags.Select(t => t.Id)
}; // include UserId property?
ConfigureEditModel(model);
return View(model);
}
[HttpPost]
public ActionResult Edit(PostViewModel model)
{
if (!ModelState.IsValid)
{
ConfigureEditModel(model);
return View(model);
}
// map your view model to a data model, save it and redirect
}
private void ConfigureEditModel(PostViewModel model)
{
model.TagsList = new SelectList(db.Tags, "Id", "Name");
model.UserList = new BlogUsers(db.Tags, "UserID", "Email"); // ??
}
Side note: Either SelectList or IEnumerable<SelectListItem> is acceptable (I find SelectList easier to read but its a millisecond or two slower because it uses reflection to generate the IEnumerable<SelectListItem>) but there is no point using the 4th parameter as you did with new SelectList(db.BlogUsers, "UserID", "Email", postsViewModel.posts.Id); - your binding to a property and the selected item will be the value of the property, so trying to set the Selected property is just ignored)
And finally the view (simplified to show only helpers without html attributes)
#model MyBlogger.ViewModel.PostViewModel
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
// #Html.HiddenFor(model => model.posts.Id) not required
#Html.LabelFor(m => m.Title)
#Html.TextBoxFor(m => m.Title)
#Html.ValidationMessageFor(m => m.Title)
// Is the UserId property required?
#Html.LabelFor(m => m.UserID)
#Html.DropDownListFor(m => m.UserID, Model.UserList, "Please select")
#Html.ValidationMessageFor(m => m.UserID)
#Html.LabelFor(model => model.SelectedTags)
#Html.ListBoxFor(m => m.SelectedTags, Model.TagsList)
// Add ValidationMessageFor() if at least one should be selected
<input type="submit" value="Save" class="btn btn-default" />
}
Side note: Since the parameter of your method is named id, the value of the id property will be added to the route parameters so it is not necessary to add a hidden input for the view models ID property (the DefaultModelBinder reads route values in addition to form values, so the view models ID property will be correctly bound (to 12 in you case)
I would like to create a simple drop-down box that displays a list of countries. The data for it comes from the database and is accessed using the entity framework data context. The user should select a country before posting the data back.(simple validation check).
I've created the view model and have also written some code, but I'm not sure about my design and I also need help to complete the code. I've done some search, but I couldn't find a simple way of doing this. I'm still getting the data from context because I am still not sure about how to use repositories. At the moment, I just want the basic drop-down to work without getting too advanced. Please help. Thanks
Updated
View Model - Country.cs
public class Country
{ [Required]
public int Id { get; set; }
public IEnumerable<SelectListItem> Countries { get; set; }
}
Controller
Public ActionResult CountriesDropDown()
{
Models.Country countryModel = new Models.Country();
using (ctx)
{
var model = (from q in ctx.Countries
select new SelectListItem
{
Text = q.CountryId,
Value = q.CountryName
}).ToList();
countryModel.Countries = model;
}
return View("Test",countryModel);
}
Countries View
#using (Html.BeginForm("DoSomething", "Test", FormMethod.Post))
{
#Html.DropDownListFor(model => model.Id, Model.Countries, "Please Select")
#Html.ValidationMessageFor(model => model.Id) //The validation part still Not Working. I want the user to select a country before submitting the form. Please help
<input type = submit value="submit" />
}
[HttpPost]
public ActionResult DoSomething(Models.Country Selectedcountry)
//country.Id holds the value of selected drop-down and it works fine
{
if (ModelState.IsValid)
//I need to do server-side validation and return back to client if Selectedcountry.Id is null (just in case, the client-side validation doesn't work)
{
return View();
}
else
{
return View("Test");
}
}
Thanks
Do this in your controller:
var model = new Country {
// assuming that the country with "id" exists
CountryId = ctx.Countries.Get(id).Id
};
model.Countries =
from q in ctx.Countries
select new SelectListItem {
Text = q.Id,
Value = q.Name
};
return view("countries", model);
Do this in your model
#model Models.Country
#Html.DropDownListFor(model => model.CountryId, model.Countries)
try this:
[Required(ErrorMessage = "Please select a Country")]
public string CountryCode{ get; set; }
public IEnumerable<SelectListItem> CountryList
{
get
{
return Country
.GetAllCountry()
.Select(Country=> new SelectListItem
{
Text = Country.Value,
Value = Country.Value
})
.ToList();
}
}
and then you could add a corresponding validation error message:
#Html.DropDownListFor(model => model.CountryCode, Model.CountryList, "select")
#Html.ValidationMessageFor(model => model.CountryCode)