You are currently browsing the monthly archive for April 2013.

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):

MVC4Edit1

After selecting different courses and saving we can see that the courses were updated when we edit the user again:

MVC4Edit2

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>&nbsp;
                <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));
        }
    }
}
Advertisements
%d bloggers like this: