Saving Many to Many relationship data on MVC Create view

asp.net-mvc asp.net-mvc-3 entity-framework many-to-many razor

Question

A Many-to-Many connection preserving the outcomes of a create view causes me some problems.

I want to construct a page for a new user profile that includes a course selection checklist (many to many relationship). My perspective utilizes the data from theCourses database and displays each one with a tick.

I want to update my database once the user uploads the data.userprofile model as well as thecourses many relationships. That is the missing code that I need!

I'm new to MVC, and despite my studying, I haven't been able to complete it.

I'm going to use this as an 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; }
}

A ViewModel was also introduced.

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

That's thecreate Controller that fills up the checkboxes for the courses:

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;
}

Here is the scene.

@{
    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). As a parameter, it getsstring[] selectedCourse about 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);
}
1
33
1/27/2014 10:23:29 AM

Accepted Answer

Edit: I've coded up this in 3 blog postings.

  • The solution is set up by part 1, who also establishes a new user.
  • The courses are added by part 2 and saved with the user profile.
  • Editing and deleting users and their courses are also possible using part 3.

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


I have made the necessary adjustments since I believe you have erred somewhat in some of your nomenclature, for instance. In my perspective, having them rendered by an Editor Template—which I describe further—is the ideal approach to have the courses submitted back as a part of the UserProfile.

This is how I would put it all into practice:

(We appreciate @Slauma for letting us know of an issue with storing new courses.)

  • Your "Courses" class in your model is a single entity and would typically be called Course; a collection of classes in your model would be called Courses.
  • Use a view model in place of having AssignedCourseData in the ViewBag.
  • Implement this when establishing a new user by include an AssignedCourseData view model in the standard Create view for a UserProfile that will be sent back along with the UserProfileViewModel.

beginning with the DB Keep the UserProfile collection's current name and give the Course collection the name Courses.

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

The OnModelCreating function should be overridden in the DbContext class. The many-to-many link between UserProfile and Course is mapped out as follows:

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);
}

In the same namespace, I would also include a fake initializer class that would provide you with some courses to start with rather than requiring you to manually add them each 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);
    }
}

To get things going, include the following code in Application Start() Global.asax:

Database.SetInitializer(new MockInitializer());

Here is the model, then:

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; }
}

To create a new user profile, generate the following 2 new action outcomes in your Controller:

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);
}

As before, here is your PopulateCourseData, but don't put it in the ViewBag since it is now a property on the UserProfileViewModel instead:

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 by adding a new folder named EditorTemplates to your ViewsShared folder, if there isn't one there already. Copy the code below, then add a new partial view named AssignedCourseData. Because the Editor template will produce all the items supplied in a collection, you don't need a for each loop to render and label all your check boxes correctly:

@model AssignedCourseData
@using MySolution.ViewModels

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

In your folder for view models, create a user profile view model. this contains a group of AssignedCourseData objects.

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

To build a user profile, add a new view named CreateUserprofile.cshtml. To do this, right-click the CreateUserProfile controller function that has already been created and choose "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>    
}

When the form is submitted back to the Controller, this will display the field names appropriately so that they are included in the user profile view model. The fields will have the following names:

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

Similar names will be used for the remaining fields, but they will be indexed with numbers 1 and 2, respectively. When the form is submitted again, follow these instructions to save the courses to the new user profile. The CreateUserProfile action result is called from this method, which you should add to your Controller, when the form is sent 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); 
        }
    }
}

EF handles the associations once the courses are a part of the user profile. For each course chosen, it will add a record to the T UserProfile Course database that was established in OnModelCreating. This CreateUserProfile action result method displays the courses that were returned:

Courses check boxes posted back to the controller

You can see that the courses have been added to the new user profile object after I choose two of them:

Courses added to new userprofile object

74
12/1/2016 9:57:16 AM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow