0

So, I've a situation where I have a Many-to-many relation between two entities. In the middle, there are fields that caracterize the relation. I will simplify the situation with the following example (in bold a simple field that is in between both entities:

public class Hobby
{
    public int HobbyID                            {get;set;}
    public string Name                            {get;set;}
    public string Type                            {get;set;}
    public ICollection<PersonHobby> PersonHobbies {get;set;}
}

public class Person
{
    public int PersonID                           {get;set;}
    public int Age                                {get;set;}
    public string Name                            {get;set;}
    public ICollection<PersonHobby> PersonHobbies {get;set;}
}

public class PersonHobby
{
    public int PersonHobbyID                      {get;set;}
    public int PersonID                           {get;set;}
    public int HobbyID                            {get;set;}
    **public int Rating                             {get;set;}**
}

What is my goal? To have a create form which lets me create a Person and immeddiatly attribute a rating to one or more Hobbies. The create form should show inputs fields for all available hobbies, but when submitting, only fields with data should be stored.

I need help to understand the best way to achieve this. So far, I tried on Person Create View and Controller to show all the hobbies in the form. The way I did this was to load all the hobbies and initialize them empty on the create controller. But when submitting I'm getting errors like: "Model bound complex types must not be abstract or value types and must have a parameterless constructor."

Thank you.

1 Answer 1

2

A little recommendation to start: I'd use view models instead of entity classes in your views, as it will provide you with more flexibility and help you achieve your goals without starting dirtying your views with code that does not belong there. You could, obviously, do it with your entity classes, providing some changes though, but I wouldn't recommend it.

The error you are getting is, I believe, because you are trying to bind to your entity classes, which define ICollection properties, and the model binder has no way to determine the concrete type that should be use to instantiate them. For a quick fix, you could change your ICollection with a concrete type such as Collection or List, for example. But again, there's that first recommendation of mine...

So, to do it the proper way, here's what I'd do:

public class PersonViewModel
{
   public int Age {get;set;}
   public string Name {get;set;}
   public List<HobbyViewModel> Hobbies {get;set;}
}

public class HobbyViewModel
{
  public int HobbyID {get;set;}
  public string Name {get;set;}
  public string Type {get;set;}
  public int? Rating {get;set;}
}

public class PersonController
   : Controller
{
   ...

   [HttpGet]
   public IActionResult Create()
   {
      PersonViewModel viewModel = new PersonViewModel();
      viewModel.Hobbies = this.DbContext.Hobbies.Select(h => new HobbyViewModel() { Name = h.Name, Type = h.Type }).ToList();
      return this.View(viewModel);
   }

   [HttpPost]
   public IActionResult Create(PersonViewModel model)
   {
      if(!this.ModelState.IsValid)
         return this.BadRequest(model);
      Person person = new Person(){ Age = model.Age, Name = model.Name };
      foreach(HobbyViewModel hobbyViewModel in model.Hobbies)
      {
        //Check if the user has rated the hobby and if not, skip it
        if(!hobbyViewModel.Rating.HasValue)
          continue;
        person.PersonHobbies.Add(new PersonHobby(){ HobbyId = hobbyViewModel.HobbyId, Rating = hobbyViewModel.Rating.Value });
      }
      this.DbContext.Persons.Add(person);
      this.DbContext.SaveChanges();
      return this.RedirectToAction(nameof(Index));
   }
}

In order words, in your person form, display a form for each of the hobbies supplied in the view model, with a Rating input. When the user posts the form, check all supplied hobbies to check if the user has rated it (ie: check if the nullable Rating property has been set) and, if she has, add a new PersonHobby to the resulting Person entity. Voila!

Finally, some recommendations on your entity models:

  • Forget about the tedious PersonHobbies property names. It is not ubiquitous, and you should always aim for ubiquity whenever possible: it makes your code prettier, easier understood and therefore easier maintainable. Name your properties Hobbies for the Person entity, and Persons or Voters for the Hobby entity, for example.
  • Rename your key properties to Id, as it will be automatically picked up by EntityFramework as the key property, with no configuration and/or data annotation. Furthermore, I expect the Person's Id property to be... well, the id of the Person. No need for specifying it.
  • I insist, to make your life easier, forget about using your entity types in views or, worst, as Json/Xml return types. You should use view models for your Views, and Data Transfer Objects (DTOs) for your Json/Xml return types. Even if this concept might seem to go against the KISS principle, believe me, it doesn't. It will just simplify your life over time, even though it might look as a pointless hassle at start. As a matter of fact, your entity types will probably evolve over time, whereas your views might not, and inversely. In addition, your views will usually require more or different info than your entity types, and keeping on this path, you might find yourself starting plumbing your views with calls and such, whereas it should simply contain view logic. Calls should be made in your controllers actions, and those should populate models with all the data/info required by your views.
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.