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();
Related
Because one of the value is not exist or equal between column, the viewmodel return nothing so i try to redirect with make another view with model only. Or maybe there is a way to open the view even one of the value is null? All solution will be accept
Thanks for your help!
// GET: Form/Details/5
[Authorize]
public ActionResult Details(int? ID)
{
if (ID == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Toolreq appRequest = db.Toolreqs.Find(ID);
List<TooldateViewModel> RequestView = new List<TooldateViewModel>();
var result = (from p in db.Toolreqs
join x in db.Toolpurposes on p.PurposeId equals x.PurposeId
join z in db.Toollists on p.ToolId equals z.ToolId
where p.ID == ID && c.sch_attr == "FEP"
select new TooldateViewModel
{
ID = p.ID,
Status = p.Status,
Date_Request = p.Date_Request,
...
sch_date = c.sch_date
}).ToList();
if (result != null)
{
return View(result);
}
if (result == null)
{
return RedirectToAction("DetailsLess");
}
return View("");
}
UPDATE!
"DetailsLess.cshtml" normal view without viewmodel that i want to redirect
#model LoginAndRegisterMVCMD5.Models.Toolreq
#{
ViewBag.Title = "DetailsLess";
}
<div>
<h4>Toolreq</h4>
<hr />
<dl class="dl-horizontal">
<dt>
#Html.DisplayNameFor(model => model.Status)
</dt>
<dd>
#Html.DisplayFor(model => model.Status)
</dd>
<dt>
#Html.DisplayNameFor(model => model.Date_Request)
</dt>
<dd>
#Html.DisplayFor(model => model.Date_Request)
</dd>
</dl>
</div>
<p>
#Html.ActionLink("Edit", "Edit", new { id = Model.ID }) |
#Html.ActionLink("Back to List", "Index")
</p>
Because one of the value is not exist or equal between column, the
viewmodel return nothing so i try to redirect with make another view
with model only. Or maybe there is a way to open the view even one of
the value is null?
Well, as per your shared code snippet it seems your appreach is correct. However, few things need to check and implemenet to make it work. Here are the details:
Firstly, Make sure, appRequest or result doesn't encounter any exception in that scenario your if (result == null) will not executed.
Secondly, Your #model LoginAndRegisterMVCMD5.Models.Toolreq on DetailsLess view must allow null result other than in between it will cought error and would not redirect.
Solution:
Your Toolreq model should allow null value just as below:
public class Toolreq
{
public string? Status { get; set; }
public string? Date_Request { get; set; }
public string? Description { get; set; }
public int? Qty { get; set; }
}
Note: Please add rest of the property based on your needs.
Controller:
Controller where you would redirect
public async Task<IActionResult> DetailsLess()
{
ViewBag.Message = "No Data Found By This ID";
return View();
}
Your Main Controller
[Authorize]
public ActionResult Details(int? ID)
{
//Set Try... Catch Block here if possibility of exception
if (ID == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Toolreq appRequest = db.Toolreqs.Find(ID);
List<TooldateViewModel> RequestView = new List<TooldateViewModel>();
var result = (from p in db.Toolreqs)... ToList();//
if (result == null)
{
return RedirectToAction("DetailsLess");
}
return View(result);
}
Output:
Note: Make sure in your view of DetailsLess accept null model value #model LoginAndRegisterMVCMD5.Models.Toolreq. To do that you can allow null to all property Like this public string? Status.
Last Result, big thanks to #Md Farid Uddin Kiron
Main Controller:
// GET: Form/Details/5
[Authorize]
public ActionResult Details(int? ID)
{
if (ID == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
List <TooldateViewModel> RequestView = new List<TooldateViewModel>();
RequestView = (from p in db.Toolreqs
join x in db.Toolpurposes on p.PurposeId equals x.PurposeId
join z in db.Toollists on p.ToolId equals z.ToolId
where p.ID == ID && c.sch_attr == "FEP"
select new TooldateViewModel
{
ID = p.ID,
Status = p.Status,
Date_Request = p.Date_Request,
...
sch_date = c.sch_date
}).ToList();
if (RequestView?.Any() != true)
{
return RedirectToAction("DetailsLess", "Form", new { ID = ID });
}
return View(RequestView);
}
The controller we aim:
// GET: Form/Details/5
public ActionResult DetailsLess(int? ID)
{
if (ID == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Toolreq toolreq = db.Toolreqs.Find(ID);
if (toolreq == null)
{
return HttpNotFound();
}
return View(toolreq);
}
I’m in the process of learning MVC with Identity 2 and I’m at the point of taking a sample project and tweaking it to fit a site that I’m trying to convert from WebForms with ASP.Net Membership provider.
What I’m having issues with at the moment is getting my head wrapped around how I get a value from another table to populate a property in a class.
I have a class called AppUser.cs and it inherits from IdentityUser. That is basically mapped to a table named “AspNetUsers” using EntityFramework. I’ve added a couple more properties (First/Last Name) to that table using EF migrations. Now I want to display what Role the user is assigned to and eventually will need to get a list of the available roles and add them to a dropdownlist. I added the RoleName property to my AppUser class but I’m assuming that since that data is not stored in the AspNetUsers table is why I’m getting an error “Invalid column name 'RoleName'”.
How would I populate the Role and/or get a list of available Roles in my AppUser class so I can display it in my View?
public class AppUser : IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string RoleName { get; set; }
}
The model on the view “Index.cshtml” is the AppUser.
#model IEnumerable<AppUser>
This is my ActionResult for my Controller.
public ActionResult Index() {
return View(UserManager.Users);
}
private AppUserManager UserManager {
get {
return HttpContext.GetOwinContext().GetUserManager<AppUserManager>();
}
}
This is the AppUserManager.cs
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using SampleMvcSite.Models;
namespace SampleMvcSite.Infrastructure {
public class AppUserManager : UserManager<AppUser> {
public AppUserManager(IUserStore<AppUser> store)
: base(store) {
}
public static AppUserManager Create(
IdentityFactoryOptions<AppUserManager> options,
IOwinContext context) {
AppIdentityDbContext db = context.Get<AppIdentityDbContext>();
AppUserManager manager = new AppUserManager(new UserStore<AppUser>(db));
manager.PasswordValidator = new CustomPasswordValidator {
RequiredLength = 6,
RequireNonLetterOrDigit = false,
RequireDigit = false,
RequireLowercase = true,
RequireUppercase = true
};
manager.UserValidator = new CustomUserValidator(manager) {
AllowOnlyAlphanumericUserNames = true,
RequireUniqueEmail = true
};
return manager;
}
}
}
UPDATED CODE:
I ended up creating a class called UserEditViewModel and I'm also including the AppRole class which was in the sample project I'm tweaking.
public class UserEditViewModel : IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string UserRole { get; set; }
public IEnumerable<AppRole> AllRoles { get; set; }
}
public class AppRole : IdentityRole {
public AppRole() : base() { }
public AppRole(string name) : base(name) { }
}
Then in my controller I did the following. Keep in mind I'm only going to allow a user to be assigned to one role at a time so theoretically there should only be the one role which is why I only called on the userRoles[0]. Since I added the RoleManager after some sample users I have a couple users that aren't assigned to a Role. I added a check to make sure the Roles had a count and if not I display "User" as their role. I will go back and add code to actually add the user to the "User" role if no roles are found.
List<UserEditViewModel> UserList = new List<UserEditViewModel>();
var users = UserManager.Users;
foreach (AppUser user in users)
{
var userRoles = await UserManager.GetRolesAsync(user.Id);
string roleName = userRoles.Count() == 0 ? "User" : userRoles[0];
UserEditViewModel editUser = new UserEditViewModel();
editUser.UserName = user.UserName;
editUser.FirstName = user.FirstName;
editUser.LastName = user.LastName;
editUser.Email = user.Email;
editUser.UserRole = roleName;
editUser.AllRoles = RoleManager.Roles;
UserList.Add(editUser);
}
return View(UserList);
Here is the View
<div class="form-group">
<label>Role:</label> #Html.DisplayNameFor(model => model.UserRole)
</div>
I would look at ViewModels. It's generally bad practice to update an entity model in your view - especially security related entities. See https://www.stevefenton.co.uk/2013/03/Why-You-Never-Expose-Your-Domain-Model-As-Your-MVC-Model/
With a viewmodel, you could have a property for List<Role> that you populate in your controller and use in your view and another property like RoleId for the user's role.
In the POST you would do something like:
if (ModelState.IsValid)
{
var roleMgr = new RoleManager<IdentityRole>(new RoleStore<IdentityRole>(context));
var userMgr = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(context));
var role = roleMgr.FindById(viewmodel.RoleId);
if (role != null) userMgr.AddToRole(viewmodel.UserId, role.Name);
}
VIEW:
<div class="form-group">
#Html.LabelFor(model => model.RoleId)
<div class="col-md-2">
#Html.DropDownListFor(model => model.RoleId, Model.RoleList, htmlAttributes: new { #class = "form-control" })
</div>
<div class="col-md-10 col-md-offset-2">
#Html.ValidationMessageFor(model => model.RoleId, "", new { #class = "text-danger" })
</div>
</div>
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)
This is microsoft's scaffolding code for the action Details of the entity MyEntity:
public async Task<ActionResult> Details(Guid? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
MyEntity myEntity = await db.MyEntities.FindAsync(id);
if (myEntity == null)
{
return HttpNotFound();
}
return View(myEntity);
}
Now let's say I want to display the name of the owner of this entity in details.cshtml, if I write the following code:
<dt>
Owner
</dt>
<dd>
#Html.DisplayFor(m => m.User.FullName)
</dd>
User shows up as null, even after trying to access Model.User to trigger the lazy loading.
Edit: Adding the model as requested
public class MyEntity
{
public Guid? Id { get; set; }
public string Name { get; set; }
public ApplicationUser User { get; set; }
}
Add .Include("User") to your linq query.
I was running into the same issue, and Matt's answer led me down a similar road. I thought I'd share what I found.
This article indicates that Lazy Loading doesn't fit well with the Async pattern. Good to know.
With that, I looked into eager loading. I found my answer there.
MyEntity myEntity = await db.MyEntities.Where(m => m.Id == id)
.Include(m => m.User)
.FirstOrDefaultAsync();
Hope that helps someone else!
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");
}