Click here to Skip to main content
15,867,686 members
Articles / Web Development / HTML

MVC 5, Entity Framework 6 and Many to Many Relationship : a step by step View Model approach

Rate me:
Please Sign up or sign in to vote.
4.91/5 (30 votes)
5 Oct 2014CPOL9 min read 231.9K   6.3K   57   37
A real world implementation of a Many to Many relation using MVC5 and Entity Framework

 

27th October 2014 Article of Day on www.asp.net

Introduction

There are a lot of articles about ASP.NET MVC and Model Binder. I didn't find any real-world article about Model Binder of Many To Many relation. So I decided to write this post and share my experience on this topic. Although I will explain easily and step by step my approach, readers of this article should have as prerequisite the knowledge of basic concepts in ASP.NET MVC as Controller, View, Model, Model Binder and Scaffolding.

Background

In this post I will use ASP.NET MVC 5, Entity Framework 6.1.1 and Visual Studio 2012. To add MVC 5 support in VS2012 you have to check this link : ASP.NET and Web Tools 2013.1 for Visual Studio 2012. You can add Entity Framework 6.1.1 through NuGet. My approach should be not the best, but it works well.

Many to Many Entity Model generation

My sample comes from a real-world application, a Web Job Portal for Employers and Job Seekers. In my sample I will use Database first approach. With Entity Framework we can start our project also with First Model Approach designing the Model of our entities and after generating the database schema using Entity Framework tool. I will not add more because this is not the topic of the post. Let's carry on. We have the following database diagram

Image 1

JobPost contains the job announce posted from Employer. Moreover the Employer can add to a JobPost many JobTag. In the database those two tables are related to each other via a link or junction table, JobPost_JobTag without payload. This table just contains the foreign keys used to link the two tables together into a many-to-many relationship. We create a new ASP.NET MVC5 web project named ManyToManyMVC5

Image 2

Now we have to add Entity Framework 6.1.1 from NuGet. We are ready to create a model and import these tables and relationship in onr project.

Add a new model to the project by right-clicking your Models folder and selecting Add>New>ADO.NET Entity Data Model.

  1. Specify as name for item "JobPortal"
  2. Select the option Generate from Database
  3. Use the wizard to create your database connection or connect to an existing one. Let "JobPortalEntities" as the name for the connection string of the Web.Config file ( the option on the buttom )
  4. Select the 3 tables to add to the model : Employer, JobPortal, JobTag and JobPortal_JobTag. Check the option "Pluralize or Singularize generated object names" and let other settings as default.

Now you see the Entity Diagram of the generated model

Image 3

Note that we have the one-to-many relationship between Employer and JobPost, and the many-to-many relationship between JobPost and JobTag. Note that the link table JobPost_JobTag is not represented as an entity in our mode. This is because our link table it has no payload ( that is it has no scalar property ). If we had a payload, we shouldn't have the many to many relationship but we should have a fourth entity with one-to-many relationship to JobPost and one to JobTag. Let's carry on. In the Models folder we have JobPortal.edmx file, that is the phisical representation for our model. We can expand it and expand JobPortal.tt. We see that the last contains our .cs files for the entitites we generated. Take a look to JobPost.cs and JobTag.cs

C#
public partial class JobPost
{
    public JobPost()
    {
        this.JobTags = new HashSet<JobTag>();
    }

    public int Id { get; set; }
    public string Title { get; set; }
    public int EmployerID { get; set; }

    public virtual Employer Employer { get; set; }
    public virtual ICollection<JobTag> JobTags { get; set; }
}
public partial class JobTag
{
    public JobTag()
    {
        this.JobPosts = new HashSet<JobPost>();
    }

    public int Id { get; set; }
    public string Tag { get; set; }

    public virtual ICollection<JobPost> JobPosts { get; set; }
}

Note that the two entities have a ICollection property to each other.

ASP.NET MVC5 and Many to Many relationship

Surfing internet you will find many interesting articles and posts on Model View Controller ASP.NET pattern and how it is easy to create MVC projects. But in the real-world the things are different and we have to enhance our approach.

In ASP.NET MVC we can use Scaffolding to quickly generating a basic outline of our application that we can edit and customize. So we can create automatically controllers and strong-typed view to perform basic CRUDL operations on our entities. Unfortunately ASP.NET MVC scaffolding doesn't handle many to many relationship. I think that the main reason is that there are too many kinds of many-to-many user interface creating/editing.

Another issue is concerning the automatic ASP.NET MVC Model Binding. As we know, model binding allows you to map HTTP request data with a model. Model binding makes it easy for you to work with form data because the request data (POST/GET) is automatically transferred into a data model you specify. ASP.NET MVC accomplishes this behind the scenes with the help of Default Binder. If we have a Many to Many relationship in the user interface we will have some kind of interface that allow user to perform multiple choice. So we need a complex type in our view to bind the selected items, as a Check Box Group or List, or a Multiple List Box. But let see the issues in action .

We will use Scaffolding to generate a basic controller with Entity Framework based CRUDL method and the related Views. Right click on Controlers foder and choise Add>Controller. In the Add Scaffold choise "MVC5 Controller with views, using Entity Framework". Set the next form as the below picture and click on Add

Image 4

Note: If you get an error message that says "There was an error getting the type...", make sure that you built the Visual Studio project after you added the class. The scaffolding uses reflection to find the class.

MVC5 Scaffolding will generate in the Controllers folder a file JobPostController.cs and in the Views folder in the JobPost subfolder the views for the related CRUDL methods. Before run the application we have to modify the RouteConfig.cs located in App_Start setting as default controller JobPostController

C#
public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "JobPost", action = "Index", id = UrlParameter.Optional }
        );
    }
}

Now you can run the application and browse the pages. It is amazing ...!! But .. we don't have any track about Many to Many relationship !!!

View Model Approach

To overcome these issues, my approach is based on ViewModel entity. View Model represents data you want to have displayed in your view. Easily is a class that represents data model used in a specific view. In our case our View Model contains the data of the JobPost, the data of all available JobTag and the data of the selected JobTag.

We start to modify the View Index ( it is associated to the Action index of the JobPostController ) and let it to show the list of the associated JobTag to a JobPost in a Listbox. So we add a row to the table in the index.cshtml.

HTML
// This row to the header table
 <th>
    @Html.DisplayNameFor(model => model.JobTags)
 </th>
// This row to the body table
 <td>
    @Html.ListBox("Id", new SelectList(item.JobTags,"Id","Tag"))
 </td>

Now we can run the apps ... in the index you will see a ListBox with all Tags for the post. It was easy.

Next step is to modify the edit view. When user edit a job post, we desire to show a ListBox with the list of available the job tags where the selected are the ones associated to the job post. User can change the selection or other data and submit back to the controller to persist the data,

The first problem is to pass to the view the list of all available job tags. The second problem is to mark as selected the associated ones. Now comes in the Model View !!

In your solution create a folder named ViewModel and add to it a class file "JobViewMode.cs" with the code below

C#
public class JobPostViewModel
{
    public JobPost JobPost { get; set; }
    public IEnumerable<SelectListItem> AllJobTags { get; set; }
 
    private List<int> _selectedJobTags;
    public List<int> SelectedJobTags
    {
        get
        {
           if (_selectedJobTags == null)
           {
              _selectedJobTags = JobPost.JobTags.Select(m => m.Id).ToList();
           }
           return _selectedJobTags;
        }
        set { _selectedJobTags = value; }
    }
}

Our view model contains a property that store a JobPost, a property that store the JobTag associated to the stored JobPost as a List<int> of JobTag's Id, and finally a property that store all available JobTag as a IEnumerable fo SelectListItem ( to bind to a ListBox )

We modify the Action edit associated to the Get as below where we introduce the JobPostViewModel instead of JobPost

C#
 // GET: /JobPost/Edit/5
public ActionResult Edit(int? id)
{
     if (id == null)
     {
       return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
     }

     var jobPostViewModel = new JobPostViewModel            {
            JobPost = _db.JobPosts.Include(i => i.JobTags).First(i => i.Id == id),
         };

     if (jobPostViewModel.JobPost == null)
        return HttpNotFound();

     var allJobTagsList = _db.JobTags.ToList();       
     jobPostViewModel.AllJobTags = allJobTagsList.Select(o => new SelectListItem
     {
                Text = o.Tag,
                Value = o.Id.ToString()
     });

     ViewBag.EmployerID =
             new SelectList(db.Employers, "Id", "FullName", jobpostViewModel.JobPost.EmployerID);
  
     return View(jobpostViewModel);
} 

Note : As you modified because you have not already changed the view type model you will get an error from the retur View field. Ignore it. It will off after you will modify the related view

In the modified action method we use the JobPostView entity. We load the propertiy JobPost with the selected job post including the eager loading of JobTags entity, and the property AllJobTags with a ListIntem builded form the JobTags and return to the View the ViewModel instead of the Model. Now we can modify the View. We change the ModelBinder to ViewModelJobPost and all the property. We add the ListBox binding.

HTML
@model ManyToManyMVC5.ViewModels.JobPostViewModel
@{
    ViewBag.Title = "Edit";
}

<h2>Edit</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    
    <div class="form-horizontal">
        <h4>JobPost</h4>
        <hr />
        @Html.ValidationSummary(true)
        @Html.HiddenFor(model => model.JobPost.Id)

        <div class="form-group">
            @Html.LabelFor(model => model.JobPost.Title, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.JobPost.Title)
                @Html.ValidationMessageFor(model => model.JobPost.Title)
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.JobPost.EmployerID, "EmployerID", new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.DropDownListFor(m => m.JobPost.EmployerID,
                        (SelectList)ViewBag.EmployerID,
                        Model.JobPost.Employer.Id);
                @Html.ValidationMessageFor(model => model.JobPost.EmployerID)
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model=>model.AllJobTags,"JobTag",new {@class="control-label col-md-2"})
            <div class="col-md-10">
                @Html.ListBoxFor(m => m.SelectedJobTags, Model.AllJobTags)
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

<script src="~/Scripts/jquery-2.1.1.min.js"></script>
<script src="~/Scripts/jquery.validate.min.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>

We can select more than one JobTag by multiselecting, as in the below

Image 5

After saved, below what we can see in out Index page

Image 6

Let take a look on the mechanism that it's behind. From the Edit page, when we save a POST command is triggered as below

Image 7

Note the the query string use the "JobPost.<FieldName>" and that we have multiple SelectedJobTag.

This is important to understand the mechanismo of the MVC autobinder. When we click on Save, the Action

           public ActionResult Edit(<code><code>JobPostViewModel jobpostView)

from the controller JobStatus is called. MVC5, becuase its ModelBinder, will automatically map the value on the query string to the related property of the class JobPostViewModel injected in the Action. We could override this mechanism for a more complex ViewModel object using the [Bind] decorator with the Action. But this is could be a subject for a new article. I hope I gave you a first idea about what is behind the scene with binding in ASP.NET MVC.

Conclusion

ASP.NET MVC5 with its Scaffolding mechanism too often has huge limitation in real world application. In this article I tryed to explain you how to use ASP.NET MVC in real world with a simple example about the implementation of a Many to Many relationship.

The example is really a basic one, but I hope that you got the idea behind the Binder and the ViewModel, so you are ready to go alone to the next level starting to modify the "create" feature in the enclosed sample project.

People could think to use other mechanisms like PropertyBag/ViewBag instead of ViewModel for example to pass other data to the View. You will lose the AutoBind mechanism and  it also is definetly not correct from the point of view of S.O.L.I.D. principles, TDD approach and Design Object Oriented in general.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
Switzerland Switzerland
I really like coding. And i really like improve my knowledge about design pattern and new architecture. I started at 13 just thirty one years ago ( self taught ) when I bought my first Sinclair ZX Spectrum. Nowadays I am a Senior Software ( Agile ) Engineer with more than 18 years of experience in Enterprise Application design and development, both back-end and front-end.
I am particularly interested in Enterprise Design Pattern, Domain Driven Design, Test Driven Design and Scrum Methodology, looking for new patterns in software development
I do not want to keep my learnings to myself, so I had joined code project, and adding few more learnings which may benefit many other software engineers and developers working in this lovely industry. I'm not going to dictate any of the points, but all the practices listed here contributed a lot in my software development career, so if you think they make some sense for you then try to adopt few. If you have any +/- comments, kindly feel free to write me back
This is a Collaborative Group

10 members

Comments and Discussions

 
QuestionSelectedProjectTags Problems! Pin
Giovanni Genovese11-May-20 23:42
Giovanni Genovese11-May-20 23:42 
QuestionCreate method Pin
DS Distribution11-Apr-19 2:12
DS Distribution11-Apr-19 2:12 
PraiseThanks!! Pin
yltsa10-Oct-18 6:03
yltsa10-Oct-18 6:03 
QuestionViewModel to Model mapping while update Pin
Member 106566586-Jun-17 19:23
Member 106566586-Jun-17 19:23 
QuestionGreat article- what about if i need to aggregate two ViewModels into one view? Pin
Member 125026821-May-17 5:11
Member 125026821-May-17 5:11 
QuestionLittle addendum to ViewModel Pin
Nathalie Desrosiers28-Mar-17 4:53
Nathalie Desrosiers28-Mar-17 4:53 
AnswerRe: Little addendum to ViewModel Pin
Member 1308262130-Mar-17 5:39
Member 1308262130-Mar-17 5:39 
AnswerRe: Little addendum to ViewModel Pin
Nathalie Desrosiers12-Apr-17 4:05
Nathalie Desrosiers12-Apr-17 4:05 
GeneralCreate Controller Pin
Nathalie Desrosiers28-Mar-17 4:51
Nathalie Desrosiers28-Mar-17 4:51 
GeneralCreate.cshtml Pin
Nathalie Desrosiers28-Mar-17 4:46
Nathalie Desrosiers28-Mar-17 4:46 
GeneralRe: Create.cshtml Pin
Member 1306055614-Apr-17 18:37
Member 1306055614-Apr-17 18:37 
QuestionPost://JobPost/Edit Pin
Nathalie Desrosiers8-Mar-17 4:09
Nathalie Desrosiers8-Mar-17 4:09 
AnswerRe: Post://JobPost/Edit Pin
a.e.k17-Apr-17 9:50
a.e.k17-Apr-17 9:50 
Questionwant to add job tag while inserting or posting Pin
Member 118866639-Jan-17 22:09
Member 118866639-Jan-17 22:09 
Questionddl problem Pin
Jose van Gonzo23-Sep-16 0:25
Jose van Gonzo23-Sep-16 0:25 
QuestionHow to create multiplies Tag from JobPost Controller Pin
mrtq8621-Jun-16 0:53
mrtq8621-Jun-16 0:53 
QuestionDownloaded Code Database Pin
madheaduk8-Jan-16 5:20
madheaduk8-Jan-16 5:20 
AnswerRe: Downloaded Code Database Pin
madheaduk8-Jan-16 5:53
madheaduk8-Jan-16 5:53 
QuestionCreate method Pin
Kotbaer9-Dec-15 6:52
Kotbaer9-Dec-15 6:52 
QuestionMVC 5, EF 6 Many to Many relationship with a link table with payload Pin
Member 36473452-Oct-15 10:45
Member 36473452-Oct-15 10:45 
QuestionWhy does the db.SaveChanges() throw DbUpdateException? Pin
Member 1160813712-Sep-15 3:30
Member 1160813712-Sep-15 3:30 
Questionview is not returning the values to edit binding controller Pin
Member 1168563728-May-15 4:57
Member 1168563728-May-15 4:57 
Thanks for the quick tutorial and I followed steps you mentioned but when time to run values to controller for db.savechanges(), the data is not coming through and the debug/control going to ViewModel class than Edit controller
ActionResult Edit(JobPostViewModel jobpostView)

my view as below and rest of the code same as in tutorial. interesting if I remove below view code it's working fine.
<div class="form-group">
    @Html.LabelFor(model => model.JobPost.Categories, "Categories", new { @class = "control-label col-md-2" })
    <div class="col-md-10">
        @Html.ListBoxFor(model => model.SelectedCategories, Model.AllCategories)
    </div>
</div>


Error at codeline
SQL
if (_selectedCategories == null)
                {
                    _selectedCategories = JobPost.Categories.Select(m => m.CategoryId).ToList();
                }


An exception of type 'System.ArgumentNullException' occurred in System.Core.dll but was not handled in user code

AnswerRe: view is not returning the values to edit binding controller Pin
MUYIDEEN KAZEEM KAZEEM8-Apr-16 14:50
professionalMUYIDEEN KAZEEM KAZEEM8-Apr-16 14:50 
QuestionWhat about Creating? Pin
Member 116575481-May-15 13:35
Member 116575481-May-15 13:35 
GeneralExcellent Article Pin
Member 114324881-Apr-15 13:47
Member 114324881-Apr-15 13:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.