35

I have some issues with a Many-to-many relationship saving the results of a create view.

I want to do a create page for a new user profile that has a checklist that lets them choose courses (many to many relationship). My view takes the records from the Courses database and shows them all with checkboxes.

Once the user posts the data, I want to update my userprofile model, and also the courses many-to-many relationship. That's the code that I have missing!

I'm new at MVC and I've been researching but I couldn't do it yet.

I am following this example: http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/updating-related-data-with-the-entity-framework-in-an-asp-net-mvc-application

This is the model:

public class UserProfile
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Courses>  usercourses { get; set; }
}

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

public class UserProfileDBContext : DbContext
{
    public DbSet<UserProfile> UserProfiles { get; set; }
    public DbSet<Courses> usrCourses{ get; set; }
}

I also added a ViewModel:

namespace Mysolution.ViewModels
{
    public class AssignedCourseData
    {
        public int CourseID { get; set; }
        public string CourseDescription { get; set; }
        public bool Assigned { get; set; }
    }
}

This is the create Controller that populates the courses checkboxes:

public ActionResult Create()
{
    PopulateCoursesData();
    return View();
}

private void PopulateCoursesData()
{
    var CoursesData = db.usrCourses;
    var viewModel = new List<AssignedCourseData>();
    foreach (var item in CoursesData)
    {
        viewModel.Add(new AssignedCourseData {
            CourseID = item.CourseID,
            CourseDescription  = item.CourseDescription,
            Assigned = false });
    }
    ViewBag.CoursePopulate = viewModel;
}

This is the view

@{
    int cnt = 0;
    List<MySolution.ViewModels.AssignedCourseData> courses = ViewBag.CoursePopulate;

    foreach (var course in courses)
    {
        <input type="checkbox" name="selectedCourse" value="@course.CourseID" /> 
        @course.CourseDescription
    }
}

And this is the controler that gets the data (and where I want to save it). It gets as a parameter string[] selectedCourse for the checkboxes:

[HttpPost]
public ActionResult Create(UserProfile userprofile, string[] selectedCourse)
{
    if (ModelState.IsValid)
    {
        db.UserProfiles.Add(userprofile);

        //TO DO: Save data from many to many (this is what I can't do!)

        db.SaveChanges();
    }

    return View(userprofile);
}
eKek0
  • 23,005
  • 25
  • 91
  • 119
PnP
  • 625
  • 1
  • 10
  • 18

1 Answers1

80

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

Ciarán Bruen
  • 5,221
  • 13
  • 59
  • 69
  • 4
    +1 Awesome detailed answer! The only thing that is missing is attaching the courses to the context, otherwise EF will create duplicate courses in the database. In `AddOrUpdateCourses`: `var course = new Course { ... }; db.Courses.Attach(course); userProfile.Courses.Add(course);` – Slauma Jul 22 '12 at 13:23
  • You're right well spotted. I did notice afterwards that there were some erroneous courses in the database but didn't have time to look into it. – Ciarán Bruen Jul 22 '12 at 13:57
  • 5
    This answer is awesome!... really good work!... thank you very much for your effort!!.. – PnP Jul 23 '12 at 16:22
  • No problem hope you find it useful. – Ciarán Bruen Jul 23 '12 at 22:23
  • Hi @CiaranBruen, Great stuff. I have one question though. Im new to ASP.NET MVC but I have used PHP MVC frameworks in the past like CakePHP & Symfony, and i remember doing this in a very simple way. Some of the scaffolded views generated the checkboxes for you. The amount of code you are having to write to make this work is ridiculous. Is there a way if i dint want to use ViewModels to do this? Is there a simpler solution than this? Thanks, – Yashvit May 08 '13 at 11:25
  • 1
    @Yash I've seen different ways where guys retrieve comma separated strings from the server, split them and create the check boxes using js, and post back cs strings again to the server. Like most scenarios there's more than one way to do this but this is the .NET MVC way, so I guess it's a matter of what you're most comfortable with. – Ciarán Bruen May 18 '13 at 20:38
  • @CiaranBruen When i try to display the checkboxes with the method you suggest, it renders just all my id's in one row. No inputfields or html, just plain text. U have any idea what I might be missing? – thomvlau May 23 '13 at 20:41
  • @thomvlau are you sure you're using the Views and editor template as described above? If you right click and view source what are you seeing? If you can paste your code online somewhere e.g. here I can take a look: http://pastebin.com – Ciarán Bruen May 24 '13 at 09:54
  • @CiaranBruen you can find my code on: http://pastebin.com/0Pb8RYRv ... If I forgot something pls tell me, but I should all be there. Thanks in advance. The Listitem in my projectcategory i filled with the right data. – thomvlau May 25 '13 at 17:24
  • 1
    @thomvlau - It appears you are missing an editor template for your AssignedCategory class. – CptRobby Jul 11 '13 at 13:31
  • @CiaranBruen how to arrange checkbox in editortemplate, in 3 columns? – Willy Jul 31 '14 at 06:06
  • ASP.NET MVC makes this about a thousand times more complicated than Django. – Chris Hawkes Aug 07 '14 at 14:37
  • @CiaranBruen, great answer! However, still a question: how do you implement mapping between ViewModel and Entity Framework objects? In this particular case there are only `Name` and `Courses` but what if the EF and its corresponding ViewModel class has may fields? How would you map them? Just new method which converts through relevant field assignments? Thanks again! – levi Dec 11 '15 at 07:08
  • @levi You could also use a tool like AutoMapper - http://automapper.codeplex.com/. In the past I've also used the built in ModelCopier in a helper method to do it, something like this (pseudo code): – Ciarán Bruen Dec 11 '15 at 16:33
  • public static UserProfile ToDomainModel(this UserProfileViewModel userProfileViewModel) { var userProfile = new UserProfile(); ModelCopier.CopyModel(userProfileViewModel, userProfile); return userProfile; } – Ciarán Bruen Dec 11 '15 at 16:34
  • And do the opposite from domain model to view model – Ciarán Bruen Dec 11 '15 at 16:38
  • @CiaranBruen, Thanks for reply. Here is my question about it. http://stackoverflow.com/questions/34218257/viewmodel-object-convert-to-entity-framework-object/34218625?noredirect=1#comment56188524_34218625 – levi Dec 12 '15 at 07:19