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.
-
Can you show us what you have so far? – Jack Jul 23 '13 at 15:23
-
@Jack: basically the create model/view/controller schema from the link, with some little exceptions and what I need is the Edit method, guess I can't add anything else that you can't see over there – JamesT Jul 23 '13 at 15:40
1 Answers
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");
}
-
Thanks for another great answer! I did have to make a small change though. I had to change data.ViewModel.Companies.ForEach( c => { c.IsSelected = data.CompanyIds.Contains(c.CompanyId); }); to foreach (var c in data.ViewModel.Companies) { c.IsSelected = data.CompanyIds.Contains(c.CompanyID); } (Sorry for the bad comment format.) – Matt Flowers Jul 24 '13 at 01:46
-
@Slauma Hey dumb question what would the code look l ike on the view ( with checkboxes ... +1 very helpful what i mean is that when using `CompanySelectViewModel` in my view i get **Error**`the model item passed into the dictionary is of type` because it is expecting `SubscriptionEditViewModel` – Don Thomas Boyle Sep 18 '13 at 15:22
-
@DonThomasBoyle: The checkboxes are `@Html.EditorFor(model => model.IsSelected)`. The `CompanySelectViewModel` is the model for an *editor template*, not for a normal view. It is described in more detail in the other post about Create (see the link in the first line of my answer above). – Slauma Sep 18 '13 at 17:10
-
@Slauma I got to fix the `checkboxes` with a `List` instead of `Ienumerable` type explained here **http://stackoverflow.com/questions/18881093/view-not-passing-ienumerable-of-model/18881644?noredirect=1#comment27896045_18881644** – Don Thomas Boyle Sep 19 '13 at 15:29
-
@Slauma this may not be to appropriate but can you take a look at http://stackoverflow.com/questions/18899681/saving-to-m-2-m-with-checkboxes I'm really struggling understanding how you have done this. – Pakk Sep 19 '13 at 16:18
-
1@DonThomasBoyle: Yes, if you use the appoach in Chris Pratt's answer you must use a List because he renders the checkboxes directly in the main view with a foreach loop. My approach was meant to use an **editor template**. To quote myself from the other answer: "It must have the name **CompanySelectViewModel.cshtml** and goes into the folder **Views/Subscription/EditorTemplates** (create such a folder manually if it doesn't exist)". In that case you can use `IEnumerable` and just `EditorFor(model => model.Companies)` without a foreach loop. – Slauma Sep 19 '13 at 19:33
-
-
@Willy: Could you ask this as a separate question? These are layout details that are too far away from the original question above in my opinion. I think it will be more a html question because editor templates are just a piece of html with a few razor expressions embedded. – Slauma Aug 03 '14 at 21:37