Freigeben über


Managing Entity Relationships with MVC Scaffolding

Using MVC Scaffolding is a great way to rapidly develop a website that provides basic CRUD functionality over your database schema. When I chose to use scaffolding on a recent project it was not entirely clear to me how it supported entity relationships and how those relationships translated to the user interface. In this post, I’ll review how the different relationship types are supported and in particular I’ll look at implementing “many to many” relationships which currently MVC Scaffolding doesn’t support out of the box.

MVC Scaffolding comes in the form of the NuGet MvcScaffolding package developed by Steve Sanderson and Scott Hanselman and you can get full details of it here. If you haven’t worked with it yet I recommend you read this first as I won’t be going into the details of what it does or how to use it in this post. I’ll be using an Entity Framework 5 model first database, MVC 4 and VS2012.

What Scaffolding Currently Supports

Let’s look at an Entity Framework model that contains the relationships we’ll be looking at in this post:

clip_image002

Which translates to:

  • A Student can belong to zero or one School only
  • A School can have zero or more students
  • A Teacher teaches zero or more students
  • A Student is taught by zero or more teachers.

When you add a controller for Student, the T4 scaffolding templates will generate an MVC view for create/edit that presents a drop list for the selection of a single School as follows:

clip_image004

That’s great and as you would expect; you can associate one school with a student or leave it undefined.

Notice the SchoolId property on Student:

clip_image006

It’s this property that will hold the drop list selection and ultimately persist to the database as a foreign key value. You wouldn’t typically add this property to your class since you have a School navigation property added for you by EF. However, its presence is essential if you want the scaffolding to create the drop list. With the EF model first approach, you’ll need to add this property in the form <entity>Id with a type of Int32 and set the Nullable property to true. In order to prevent EF from creating the foreign key database column for you which you won’t be able to map to this property, you’ll need to create a foreign key constraint between School and Student manually. On the properties dialog for the association, click the ellipses and define the constraint as shown below:

clip_image008

Now what about the association the other way, where a School can be associated with many students? Let’s look at the create/edit views that the scaffolding templates generate for School:

clip_image010

You can see there is no ability to select which students can be associated with the School. Incidentally, this would be the same if the association where one to many (i.e. a Student can belong to one and only one School). In that case you would not want to be able to select students here since you would have to cater for a student being completely disassociated with any School which would cause a foreign key violation.

For a zero or one scenario, this is not an issue and if you want to allow the user to associate zero or more students with a School you’re going to have to implement some sort of multi select functionality yourself. The same is true for the many to many association between School and Teacher where you also need the ability to select one or more schools or teachers. You can already see above that the Student create/edit view does not allow you to select teachers. For the Teacher create/edit views, you’ll find selection of students is also absent.

Supporting Association With More Than One Entity – The “Many” End Of the Association

So at present the scaffolding templates don’t support generating views that require the selection and association with more than one entity – the “many” end of the association. Adding this functionality to the generated code is not difficult and in this section we’ll look at how this is done. I’m going to show how to allow one or more teachers to be associated with a Student. The same principle can be applied to the association in the opposite direction and also for the “many” end association between School and Student and in the example solution this has been implemented.

Modifying the Model

First we need to add a property to the model that will hold the selection of teachers. This can be defined as an array of integers representing teacher ids. For a production solution I would recommend that the EF model classes and the MVC view model classes are different types and that you convert between the two using some form of mapping code. However that’s a subject for another post and for brevity we’ll use a partial class to hold the additional property. Let’s add a new partial class to the project containing your generated EF classes:

 public partial class Student
{
    public int[] TeacherIds { get; set; }
}

Modifying the Controller

For this example, the controller code I’ve generated uses the Repository Pattern. Using the repository pattern means any data access code is abstracted out of the controller and placed in a repository class that is referenced through an interface. This has many advantages but the key one for me is that you can use dependency injection when unit testing your controller methods. You can select the repository pattern template in the Add controller dialog as follows:

clip_image012

Now let’s modify the generated controller code. For the create operation, two StudentController methods will require modification. The first is the Create method that returns the create view. We need to instantiate a Student model and initialise the new TeacherIds property with an empty array. We also need to get a list of all possible teachers from the teachers repository and save this in the ViewBag. I’m not a great fan of stuffing data in the ViewBag – I would prefer to see this as property on the view model, but again for brevity we’ll do it this way:

 public ActionResult Create()
{
    ViewBag.PossibleSchools = schoolRepository.All;

    Student model = new Student
    {
        TeacherIds = new int[0]
    };

    ViewBag.PossibleTeachers = teacherRepository.All;

    return View(model);
}

For the post back Create method, the selected teachers will be passed back in the TeacherIds property, something the MVC property binding magic will take care of for us. We need to take those ids and convert them to Teacher objects that can then be associated with the Student before persisting the Student entity:

 [HttpPost]
public ActionResult Create(Student student)
{
    if (ModelState.IsValid) {

        if (student.TeacherIds != null)
        {
            student.Teachers = (from t in this.teacherRepository.All where student.TeacherIds.Contains(t.TeacherId) select t).ToList();
        }

        studentRepository.InsertOrUpdate(student);
        studentRepository.Save();
        return RedirectToAction("Index");
    } else {

        ViewBag.PossibleSchools = schoolRepository.All;
        ViewBag.PossibleTeachers = teacherRepository.All;

        return View(student);
    }
}

The teacherRepository instance variable will need to be added since the template will only add schoolRepository and studentRepository instance variables. By default, each repository has its own DbContext instance (our generated subclass of this is ModelContainer). When the studentRepository.Save() method above is called, you will now get the following exception:

An entity object cannot be referenced by multiple instances of IEntityChangeTracker

This is because you have two independent contexts tracking Student and Teacher entities – not an issue with the original code but with the modifications we have made you now have a small problem. Essentially you need to share the same context between all repositories. You can achieve this in a number of ways and one simple way is to replace the ModelContainer instantiation in each repository class with the following:

 ModelContainer context = HttpContext.Current.Items["ModelContainer"] as ModelContainer; 

and in the MvcApplication class, instantiate a ModelContainer instance by adding the following:

 protected void Application_BeginRequest(Object sender, EventArgs e) 

{ 

    HttpContext.Current.Items["ModelContainer"] = new ModelContainer(); 

}

You’ll now have the ModelContainer instance created once per request and shared between all repositories.

Modifying the View

For the view, we can implement the selection of multiple teachers in a number of ways. You could use the HtmlHelper.ListBoxFor approach and generate an HTML select list that allows for multiple selections. However I found that selecting and deselecting multiple values from a drop down, particularly a long drop down wasn’t a great user experience. I think a better alternative is to generate a list of checkboxes and if the list is long you can place it in a scrollable section.

Let’s look at the additional view mark-up we need to append to the Student _CreateOrEdit.cshtml:

 <div class="editor-label">
    @Html.LabelFor(model => model.TeacherIds, "Teachers")
</div>

<div class="editor-field">
    <ul>
        @foreach (DatabaseEntities.Teacher teacher in ViewBag.PossibleTeachers)
        {
            <li>                        
                @if (Model.Teachers.Select(e => e).Where(e => e.TeacherId == teacher.TeacherId).Count() > 0)
                {
                   <input type="checkbox" name="TeacherIds" value="@teacher.TeacherId" checked="checked" />
                } 
                else
                {
                   <input type="checkbox" name="TeacherIds" value="@teacher.TeacherId" />
                }
                @teacher.Name
            </li>
        }
        @Html.ValidationMessageFor(model => model.TeacherIds)
    </ul>
</div>

This will create a labelled check box for each teacher. In the case of the view being returned for an edit, the LINQ query will determine whether the teacher is currently in the Model.Teachers collection and if present, set the checkbox to be checked. This is what the view looks like when the user creates a Student for the first time:

clip_image014

You’ll notice that in order for the label to appear as “Teachers” and not “TeacherIds”, I’ve added the following mark-up:

 @Html.LabelFor(model => model.TeacherIds, "Teachers")

Ideally, you’d want to place a [Display(Name = "Teachers")] attribute on you model property to set the display name. However, because we are using a generated EF class we can’t do this. That’s another good reason to split your MVC model and EF model into different classes.

And Finally, Modifying the Controller for Editing

The final piece of the puzzle is to add the functionality in the controller to support editing the associations. Again we need to modify two methods - first the edit method that returns the edit view. We need to load the required Student from the repository using the supplied student id and then initialise the TeacherIds property with the ids of the teachers that are currently associated with the Student. We also need to get a list of all possible teachers from the teachers repository and save this in the ViewBag:

 public ActionResult Edit(int id)
{
   ViewBag.PossibleSchools = schoolRepository.All;

   ViewBag.PossibleTeachers = teacherRepository.All;

   Student model = studentRepository.Find(id);

   model.TeacherIds = (from t in model.Teachers select t.TeacherId).ToArray();

   return View(model);
}

For the post back Edit method, as for the post back Create method, the selected teachers will be passed back in the TeacherIds property and we need to take those ids and convert them to Teacher objects that can then be associated with the Student before persisting the Student entity. However we need to do a bit more for EF to update our associations table as simply modifying the Teachers collection on Student is not enough. What needs to happen is that we need to load the original Student back from the repository and modify the returned collection. We will be persisting this instance and therefore we need to copy all properties from the MVC supplied model to the original Student read back from the repository. If you have split your MVC and EF model classes, this is where a call to your mapping code will come in handy. Using this pattern ensures EF now updates our many to many associations in the association table:

 [HttpPost]
public ActionResult Edit(Student student)
{
   if (ModelState.IsValid) {
       Student originalStudent = this.studentRepository.Find(student.StudentId);

       originalStudent.Grade = student.Grade;
       originalStudent.Name = student.Name;
       originalStudent.SchoolId = student.SchoolId;

       originalStudent.Teachers.Clear();

       if (student.TeacherIds != null)
       {
           originalStudent.Teachers = (from t in this.teacherRepository.All where student.TeacherIds.Contains(t.TeacherId) select t).ToList();
       }

       studentRepository.InsertOrUpdate(originalStudent);
       studentRepository.Save();
       return RedirectToAction("Index");
   } else {
       ViewBag.PossibleSchools = schoolRepository.All;
       ViewBag.PossibleTeachers = teacherRepository.All;
       return View(student);
   }
}

Conclusion

MVC Scaffolding is a great way to kick start development of your MVC web application. However as you have seen if you want to manage entity relationships fully, you are going to have to do a little bit of work. If you apply the changes above to the School and Teachers controllers and views (as in the example solution) you should have the functionality that is shown in the following views:

clip_image016

You can download the example solution here.

Written by Christopher Owczarek

Comments

  • Anonymous
    December 31, 2013
    Very good, I will use a lot!

  • Anonymous
    April 20, 2014
    Great article - but my problem is that I've already built my models (without repositories) and added lots of code to do other stuff - if I do it your way I'd have to recreate my models and scaffolded items and re-add my code again which will be a huge job.  (Unfortunately I couldn't foresee this in the design as my client realised late on they wanted extra information which meant creating a many-to-many relationship to my DB.) I guess my question is this: is there any way I can add this m:m support to an existing MVC site (modelled without repositories) without having to essentially rebuild my whole site?

  • Anonymous
    April 23, 2014
    That was a great article. It helped verry much!

  • Anonymous
    May 12, 2014
    i have 20+ entities is it possible to create 20+ controllers automatically using scaffolding. i am feeling very painful to add controller every time.

  • Anonymous
    March 28, 2015
    How do I add manage related entities using add/remove buttons??