Source now available on github:
https://github.com/cbruen1/mvc4-many-to-many
So finally we are at the third part of our MVC EF many to many trilogy. Part 1 was the initial setup of the solution where we could add new users without any courses. In part 2 I then showed how to add courses and save one or more courses as part of a user profile. In this part I’ll show how to edit a user and change their name and the courses they’ve been registered for, display a user’s details in read only mode, and delete a user from the app.
The first thing we have to do for this is add 2 new controller actions for Edit, one Get and one Post (the Get retrieves the UserProfile for editing and the Post posts it back for saving to the db). Paste the following code into the UserProfileController file after the Create actions (created in part 1 and present in the code solution):
public ActionResult Edit(int id = 0) { // Get all courses var allDbCourses = db.Courses.ToList(); // Get the user we are editing and include the courses already subscribed to var userProfile = db.UserProfiles.Include("Courses").FirstOrDefault(x => x.UserProfileID == id); var userProfileViewModel = userProfile.ToViewModel(allDbCourses); return View(userProfileViewModel); } [HttpPost] public ActionResult Edit(UserProfileViewModel userProfileViewModel) { if (ModelState.IsValid) { var originalUserProfile = db.UserProfiles.Find(userProfileViewModel.UserProfileID); AddOrUpdateCourses(originalUserProfile, userProfileViewModel.Courses); db.Entry(originalUserProfile).CurrentValues.SetValues(userProfileViewModel); db.SaveChanges(); return RedirectToAction("Index"); } return View(userProfileViewModel); }
Next we need to update our existing AddOrUpdateCourses method. When saving new courses for an existing user we first delete the already saved ones and add any newly selected ones:
private void AddOrUpdateCourses(UserProfile userProfile, IEnumerable<AssignedCourseData> assignedCourses) { if (assignedCourses == null) return; if (userProfile.UserProfileID != 0) { // Existing user - drop existing courses and add the new ones if any foreach (var course in userProfile.Courses.ToList()) { userProfile.Courses.Remove(course); } foreach (var course in assignedCourses.Where(c => c.Assigned)) { userProfile.Courses.Add(db.Courses.Find(course.CourseID)); } } else { // New user foreach (var assignedCourse in assignedCourses.Where(c => c.Assigned)) { var course = new Course { CourseID = assignedCourse.CourseID }; db.Courses.Attach(course); userProfile.Courses.Add(course); } } }
We also update our UserProfileViewModel class in our Models folder to initialise the Courses collection in the constructor:
public class UserProfileViewModel { public UserProfileViewModel() { Courses = new Collection<AssignedCourseData>(); } public int UserProfileID { get; set; } public string Name { get; set; } public virtual ICollection<AssignedCourseData> Courses { get; set; } }
Next we update our ViewModelHelper class to allow us add and update courses for a user:
public static class ViewModelHelpers { public static UserProfileViewModel ToViewModel(this UserProfile userProfile) { var userProfileViewModel = new UserProfileViewModel { Name = userProfile.Name, UserProfileID = userProfile.UserProfileID }; foreach (var course in userProfile.Courses) { userProfileViewModel.Courses.Add(new AssignedCourseData { CourseID = course.CourseID, CourseDescription = course.CourseDescripcion, Assigned = true }); } return userProfileViewModel; } public static UserProfileViewModel ToViewModel(this UserProfile userProfile, ICollection<Course> allDbCourses) { var userProfileViewModel = new UserProfileViewModel { Name = userProfile.Name, UserProfileID = userProfile.UserProfileID }; // Collection for full list of courses with user's already assigned courses included ICollection<AssignedCourseData> allCourses = new List<AssignedCourseData>(); foreach (var c in allDbCourses) { // Create new AssignedCourseData for each course and set Assigned = true if user already has course var assignedCourse = new AssignedCourseData { CourseID = c.CourseID, CourseDescription = c.CourseDescripcion, Assigned = userProfile.Courses.FirstOrDefault(x => x.CourseID == c.CourseID) != null }; allCourses.Add(assignedCourse); } userProfileViewModel.Courses = allCourses; return userProfileViewModel; } public static UserProfile ToDomainModel(this UserProfileViewModel userProfileViewModel) { var userProfile = new UserProfile { Name = userProfileViewModel.Name, UserProfileID = userProfileViewModel.UserProfileID }; return userProfile; } }
Next we add the Edit view that allows us to edit each user and change their name and assigned courses. In the 2nd Edit controller action created previously right click and select “Add View”:
– Name the View “Edit”
– Select the Razor View engine
– Click “Create strongly typed view” and select the UserProfileViewModel class as the Model class
– In the scaffold template select “Edit”. Check the “Use a layout or master page” check box
In the resulting Edit.cshtml change the line “@Html.EditorFor(model => model.UserProfileID)” to “@Html.DisplayFor(model => model.UserProfileID)”. This is the unique id field and we don’t want to change this, only display it. We also need to add the lines “@Html.HiddenFor(x => x.UserProfileID)” which makes sure our UserProfileID is posted back, and “@Html.EditorFor(x => x.Courses)” which renders the courses for a user. Here’s the full Edit.cshtml view:
@model MVC4ManyToMany.Models.ViewModels.UserProfileViewModel @{ ViewBag.Title = "Edit"; } <h2>Edit</h2> @using (Html.BeginForm("Edit", "UserProfile", new { id="formEdit", name="formEdit" })) { @Html.ValidationSummary(true) <fieldset> <legend>UserProfileViewModel</legend> @Html.HiddenFor(x => x.UserProfileID) <div> @Html.LabelFor(model => model.UserProfileID) </div> <div> @Html.DisplayFor(model => model.UserProfileID) </div> <div> @Html.LabelFor(model => model.Name) </div> <div> @Html.EditorFor(model => model.Name) @Html.ValidationMessageFor(model => model.Name) </div> @* Render the check boxes using the Editor Template *@ @Html.EditorFor(x => x.Courses) <p> <input type="submit" value="Save" /> </p> </fieldset> } <div> @Html.ActionLink("Back to List", "Index") </div> @section Scripts { @Scripts.Render("~/bundles/jqueryval") }
We also change our UserProfile\Index.cshtml view to pass the correct id’s to the controller – update the following lines:
@Html.ActionLink("Edit", "Edit", new { id=item.UserProfileID }) | @Html.ActionLink("Details", "Details", new { id=item.UserProfileID }) | @Html.ActionLink("Delete", "Delete", new { id=item.UserProfileID })
Now we have a view that will allow us to edit users after initial creation. Run the application which takes us to the main Index page and click on the link to the UserProfile Index page (if you get a timeout error the first time click refresh). If there are already users present click on any Edit link which will take us to the Edit page that we added earlier (otherwise create a user first):
After selecting different courses and saving we can see that the courses were updated when we edit the user again:
So far so good. To complete our functionality we need to add Details and Delete views and actions. In our UserProfileController add a new controller action called Details:
public ActionResult Details(int id = 0) { // Get all courses var allDbCourses = db.Courses.ToList(); // Get the user we are editing and include the courses already subscribed to var userProfile = db.UserProfiles.Include("Courses").FirstOrDefault(x => x.UserProfileID == id); var userProfileViewModel = userProfile.ToViewModel(allDbCourses); return View(userProfileViewModel); }
In the Details controller action right click and select “Add View”:
– Name the View “Details”
– Select the Razor View engine
– Click “Create strongly typed view” and select the UserProfileViewModel class as the Model class
– In the scaffold template select “Details”. Check the “Use a layout or master page” check box
Update the Details.cshtml view with the following code:
@model MVC4ManyToMany.Models.ViewModels.UserProfileViewModel @{ ViewBag.Title = "Details"; } <h2>Details</h2> <fieldset> <legend>UserProfileViewModel</legend> <div> @Html.DisplayForModel(Model) <p></p> @foreach (var course in Model.Courses) { <div> <span>@Html.CheckBox("Blah", course.Assigned, new { disabled = "disabled" })</span> <span>@course.CourseDescription</span> </div> } </div> </fieldset> <p> @Html.ActionLink("Edit", "Edit", new { id=Model.UserProfileID }) | @Html.ActionLink("Back to List", "Index") </p>
After compiling refresh the app and click on the Details link of any user in our Index view, which will display a read only view of the user details and the courses they are subscribed to.
Finally the Delete view – this requires a little bit more work on the controller but nothing too hairy. In our UserProfileController add a new controller action called Delete:
public ActionResult Delete(int id = 0) { var userProfileIQueryable = from u in db.UserProfiles.Include("Courses") where u.UserProfileID == id select u; if (!userProfileIQueryable.Any()) { return HttpNotFound("User not found."); } var userProfile = userProfileIQueryable.First(); var userProfileViewModel = userProfile.ToViewModel(); return View(userProfileViewModel); }
This is the initial Get action that retrieves the user we’re deleting and displays the details in a Details view (that we will add last). We need another controller action to handle the deletion when the user clicks the Delete button on our view, so add the following code:
[HttpPost, ActionName("Delete")] public ActionResult DeleteConfirmed(int id) { var userProfile = db.UserProfiles.Include("Courses").Single(u => u.UserProfileID == id); DeleteUserProfile(userProfile); return RedirectToAction("Index"); } private void DeleteUserProfile(UserProfile userProfile) { if (userProfile.Courses != null) { foreach (var course in userProfile.Courses.ToList()) { userProfile.Courses.Remove(course); } } db.UserProfiles.Remove(userProfile); db.SaveChanges(); }
Next right click in the Delete action and select “Add View”, as we’ve previously done for Edit:
– Name the View “Delete”
– Select the Razor View engine
– Click “Create strongly typed view” and select the UserProfileViewModel class as the Model class
– In the scaffold template select “Delete”. Check the “Use a layout or master page” check box
The resulting Razor code should be fine as is, so when we re-compile and re-load the Index page we should have a link to our Delete page.
So with the addition of edit and delete functionality that gives us a fully working CRUD (CReate /Update / Delete) application. To recap again on what we’ve achieved with these 3 posts:
– Multi tier MVC4 project utilising Entity Framework 5.
– Creating a many to many table structure in our database using code first
– Adding child collections to our objects that lets Users have multiple Courses and Courses have multiple Users
– Using an Editor Template to render child collections and naming the fields in the correct format for the MVC model binder
As mentioned in a previous post this isn’t a fully fledged enterprise level application but is a good foundation for a CRUD application. To make the code more robust and complete and have it closer to industry standards we would add unit testing, dependency injection / IoC, error handling, logging, etc.
EDIT 1
To display the courses for each user in the Index.cshtml view update the foreach loop like this:
@foreach (var item in Model) { <tr> <td> @Html.DisplayFor(modelItem => item.UserProfileID) </td> <td> @Html.DisplayFor(modelItem => item.Name) </td> <td> <ul> @foreach (var course in item.Courses) { <li>@course.CourseDescription</li> } </ul> </td> <td> @Html.ActionLink("Edit", "Edit", new { id=item.UserProfileID }) | @Html.ActionLink("Details", "Details", new { id=item.UserProfileID }) | @Html.ActionLink("Delete", "Delete", new { id=item.UserProfileID }) </td> </tr> }
Edit 2
To only remove existing courses that have been unchecked and add new ones (instead of deleting all courses each time) we can do this:
private void AddOrUpdateKeepExistingCourses(UserProfile userProfile, IEnumerable<AssignedCourseData> assignedCourses) { var webCourseAssignedIDs = assignedCourses.Where(c => c.Assigned).Select(webCourse => webCourse.CourseID); var dbCourseIDs = userProfile.Courses.Select(dbCourse => dbCourse.CourseID); var courseIDs = dbCourseIDs as int[] ?? dbCourseIDs.ToArray(); var coursesToDeleteIDs = courseIDs.Where(id => !webCourseAssignedIDs.Contains(id)).ToList(); // Delete removed courses foreach (var id in coursesToDeleteIDs) { userProfile.Courses.Remove(db.Courses.Find(id)); } // Add courses that user doesn't already have foreach (var id in webCourseAssignedIDs) { if (!courseIDs.Contains(id)) { userProfile.Courses.Add(db.Courses.Find(id)); } } }
12 comments
Comments feed for this article
April 22, 2013 at 11:23 am
Alen
Thanks for the nice article. I have a question. In the UserProfile/Index.cshtml
how can i get users with their assigned courses. Actually i add Courses and @Html.DisplayFor(modeltItem => item.Courses) but i see the ids of the assigned courses instead of descriptions and can’t see Courses.CourseDescription in intellisense? Thanks.
April 28, 2013 at 8:22 am
codenodes
Hi Alen sorry for the delay but I’ve been having VS issues. I tried to install the latest VS upgrade but it failed and now my VS is completely borked! I’ll try to get back to you when I have it sorted…cheers, Ciaran
May 3, 2013 at 4:46 pm
codenodes
I’ve updated the post to display the courses in the Index.cshtml view – see Edit 1
May 7, 2013 at 6:22 am
Alen
It’s great. Thank you very much
May 31, 2013 at 12:36 pm
Alen
Here i can show first student’s name and then assigned courses in the list for that student. How can i make it to show first the course and then assigned students to that course in the list. I made some changes Courses to UserProfile and vice versa and editor template to show student names instead of courses with checkboxes in the code and getting the course and students but when i add new student to course it creates duplicate course with new student. For example I have History with a student name John, when i add Mike it shows History — John, History —–Mike. I want to show History —— John – Mike.. etc. Simply couldnt change the place of userprofile with course int the project.
April 26, 2013 at 12:58 am
Joe
Nice article – can’t seem to download part 3 though?
April 26, 2013 at 9:51 am
codenodes
Hi Joe you’re right sorry about that don’t know why it stopped working. I uploaded it to box.net again so it seems to work now…cheers, Ciaran
April 28, 2013 at 1:53 am
Joe
Thanks for that Cairan. Really useful. I’ve taken a lot from this series so thanks again.
April 28, 2013 at 8:25 am
codenodes
No problem glad you found it useful.
April 29, 2013 at 4:24 pm
Joe
Hi Ciaran,
Regarding the AddOrUpdateCourses method, I notice that your code removes all existing courses and then adds all assigned courses.
Could you suggest a way of only removing existing courses that are not assigned and only adding new courses that are assigned?
I think I’ll have to do it this way for my own requirement even though I realise your way is better.
Thanks.
May 4, 2013 at 10:26 am
codenodes
Hi Joe, I’ve updated the post to show how to do this – see Edit 2. I hope that’s what you were looking for.
August 5, 2015 at 1:47 am
dush_a
Great article.
I followed it but now I have a real problem in the View. In you sample you use checkboxes to display many courses, but if there are hundreds of courses to select this is not practical. Then we want to show only the selected courses in a grid/table and it must be able to edit columns and add rows, and provide available courses in a dropdown list when adding or updating. There are few samples for inline editing using WebGrid for single tables but I am now stuck with this many-to-many scenario. Is it possible for you to show your sample with a table/grid ?