Saving Many to Many relationship data on MVC Create view

Edit: I’ve written this up in 3 blog posts with code

  • part 1 sets up the solution and creates a new user
  • part 2 adds the courses and saves them with the user profile
  • part 3 allows editing and deletion of users and their courses

Github source:
https://github.com/cbruen1/mvc4-many-to-many


I think you’ve strayed from conventions a little bit in some of your naming for example so I’ve made changes where I saw fit. In my opinion the best way to have the courses posted back as part of the UserProfile is to have them rendered by an Editor Template which I explain further on.

Here’s how I would implement all this:

(Thanks to @Slauma for pointing out a bug when saving new courses).

  • in your model, your “Courses” class is a single entity and would usually be named Course, and a collection of class Course would be named Courses.
  • instead of having AssignedCourseData in the ViewBag use a view model
  • implement this while creating a new user – i.e. have a standard Create view for a Userprofile containing an AssignedCourseData view model which will be posted back with the UserProfileViewModel.

Starting from the DB leave the UserProfile collection as is and name the Course collection Courses:

public DbSet<UserProfile> UserProfiles { get; set; }
public DbSet<Course> Courses { get; set; }

In the DbContext class override the OnModelCreating method. This is how you map the many to many relationship between UserProfile and Course:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<UserProfile>()
        .HasMany(up => up.Courses)
        .WithMany(course => course.UserProfiles)
        .Map(mc =>
        {
            mc.ToTable("T_UserProfile_Course");
            mc.MapLeftKey("UserProfileID");
            mc.MapRightKey("CourseID");
        }
    );

    base.OnModelCreating(modelBuilder);
}

I would also add a mock initializer class in the same namespace that will give you some courses to start with and means you don’t have to manually add them every time your model changes:

public class MockInitializer : DropCreateDatabaseAlways<MVC4PartialViewsContext>
{
    protected override void Seed(MVC4PartialViewsContext context)
    {
        base.Seed(context);

        var course1 = new Course { CourseID = 1, CourseDescripcion = "Bird Watching" };
        var course2 = new Course { CourseID = 2, CourseDescripcion = "Basket weaving for beginners" };
        var course3 = new Course { CourseID = 3, CourseDescripcion = "Photography 101" };

        context.Courses.Add(course1);
        context.Courses.Add(course2);
        context.Courses.Add(course3);
    }
}

Add this line to Application_Start() Global.asax to kick start it:

Database.SetInitializer(new MockInitializer());

So here’s the model:

public class UserProfile
{
    public UserProfile()
    {
        Courses = new List<Course>();
    }
    public int UserProfileID { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Course> Courses { get; set; }
}

public class Course
{
    public int CourseID { get; set; }
    public string CourseDescripcion { get; set; }
    public virtual ICollection<UserProfile> UserProfiles { get; set; }
}

Now create 2 new action results in your Controller to create a new user profile:

public ActionResult CreateUserProfile()
{
    var userProfileViewModel = new UserProfileViewModel { Courses = PopulateCourseData() };

    return View(userProfileViewModel);
}

[HttpPost]
public ActionResult CreateUserProfile(UserProfileViewModel userProfileViewModel)
{
    if (ModelState.IsValid)
    {
        var userProfile = new UserProfile { Name = userProfileViewModel.Name };

        AddOrUpdateCourses(userProfile, userProfileViewModel.Courses);
        db.UserProfiles.Add(userProfile);
        db.SaveChanges();

        return RedirectToAction("Index");
    }

    return View(userProfileViewModel);
}

Here’s your PopulateCourseData similar to how you had it except don’t put in in the ViewBag – it’s now a property on the UserProfileViewModel:

private ICollection<AssignedCourseData> PopulateCourseData()
{
    var courses = db.Courses;
    var assignedCourses = new List<AssignedCourseData>();

    foreach (var item in courses)
    {
        assignedCourses.Add(new AssignedCourseData
        {
            CourseID = item.CourseID,
            CourseDescription = item.CourseDescripcion,
            Assigned = false
        });
    }

    return assignedCourses;
}

Create an Editor Template – in your Views\Shared folder create a new folder called EditorTemplates if you don’t already have one. Add a new partial view called AssignedCourseData and paste the code below. This is the bit of magic that renders and names all your check boxes correctly – you don’t need a for each loop as the Editor template will create all the items passed in a collection:

@model AssignedCourseData
@using MySolution.ViewModels

<fieldset>
    @Html.HiddenFor(model => model.CourseID)    
    @Html.CheckBoxFor(model => model.Assigned)
    @Html.DisplayFor(model => model.CourseDescription)
</fieldset>

Create a user profile view model in your view models folder – this has a collection of AssignedCourseData objects:

public class UserProfileViewModel
{
    public int UserProfileID { get; set; }
    public string Name { get; set; }
    public virtual ICollection<AssignedCourseData> Courses { get; set; }
}

Add a new view called CreateUserprofile.cshtml to create a user profile – you can right click in the already added CreateUserProfile controller method and select “Add View”:

@model UserProfileViewModel
@using MySolution.ViewModels

@using (Html.BeginForm("CreateUserProfile", "Course", FormMethod.Post))
{
    @Html.ValidationSummary(true)

    <fieldset>

        @Html.DisplayFor(model => model.Name)
        @Html.EditorFor(model => model.Name)

        // Render the check boxes using the Editor Template
        @Html.EditorFor(x => x.Courses)

    </fieldset>

    <p>
        <input type="submit" value="Create" />
    </p>    
}

This will render the field names correctly in order that they are part of the user profile view model when the form is posted back to the Controller. The fields will be named as such:

<fieldset>
    <input data-val="true" data-val-number="The field CourseID must be a number." data-val-required="The CourseID field is required." id="Courses_0__CourseID" name="Courses[0].CourseID" type="hidden" value="1" />    
    <input data-val="true" data-val-required="The Assigned field is required." id="Courses_0__Assigned" name="Courses[0].Assigned" type="checkbox" value="true" /><input name="Courses[0].Assigned" type="hidden" value="false" />
    Bird Watching 
</fieldset>

The other fields will be named similarly except will be indexed with 1 and 2 respectively. Finally here’s how to save the courses to the new user profile when the form is posted back. Add this method to your Controller – this is called from the CreateUserProfile action result when the form is posted back:

private void AddOrUpdateCourses(UserProfile userProfile, IEnumerable<AssignedCourseData> assignedCourses)
{
    foreach (var assignedCourse in assignedCourses)
    {
        if (assignedCourse.Assigned)
        {
            var course = new Course { CourseID = assignedCourse.CourseID }; 
            db.Courses.Attach(course); 
            userProfile.Courses.Add(course); 
        }
    }
}

Once the courses are part of the user profile EF takes care of the associations. It will add a record for each course selected to the T_UserProfile_Course table created in OnModelCreating. Here’s the CreateUserProfile action result method showing the courses posted back :

Courses check boxes posted back to the controller

I selected 2 courses and you can see that the courses have been added to the new user profile object:

Courses added to new userprofile object

Leave a Comment