ASP.NET MVC 核心 - 多对多关系 - 创建视图

ASP.NET MVC CORE - Many to many relations - Create Views

所以,我遇到了两个实体之间存在多对多关系的情况。在中间,有表征关系的字段。我将通过以下示例简化情况(粗体显示两个实体之间的简单字段:

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

我的目标是什么?拥有一个创建表单,让我可以创建一个人并立即将评级归因于一个或多个爱好。创建表单应显示所有可用爱好的输入字段,但在提交时,应仅存储包含数据的字段。

我需要帮助来了解实现此目标的最佳方法。到目前为止,我尝试在 Person Create View 和 Controller 上显示表单中的所有爱好。我这样做的方法是加载所有爱好并在创建控制器上将它们初始化为空。但是在提交时我收到如下错误:“模型绑定的复杂类型不能是抽象类型或值类型,并且必须具有无参数构造函数。”

谢谢。

开始时的一点建议:我会在您的视图中使用视图模型而不是实体 类,因为它将为您提供更大的灵活性并帮助您实现目标,而不会开始用代码弄脏您的视图那不属于那里。显然,你可以用你的实体 类 来做,但提供一些更改,但我不推荐它。

我相信你得到的错误是因为你试图绑定到你的实体 类,它定义了 ICollection 属性,并且模型绑定器有无法确定应该用于实例化它们的具体类型。为了快速修复,您可以将 ICollection 更改为具体类型,例如 CollectionList,例如。但同样,这是我的第一个建议...

所以,为了以正确的方式做到这一点,我会这样做:

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

换句话说,在您的个人表单中,为视图模型中提供的每个爱好显示一个表单,并带有评级输入。当用户发布表单时,检查所有提供的爱好以检查用户是否对其进行了评分(即:检查是否已设置可为空的 Rating 属性),如果已设置,则将新的 PersonH​​obby 添加到结果 Person实体。瞧!

最后,对您的实体模型提出一些建议:

  • 忘掉乏味的 PersonH​​obbies 属性 名字吧。它不是无处不在你应该尽可能追求无处不在:它使你的代码更漂亮,更容易理解,因此更容易维护。例如,将 Person 实体的属性命名为 Hobbies,并将 Hobby 实体命名为 Persons 或 Voters。
  • 将您的密钥属性重命名为 Id,因为它将被 EntityFramework 自动选取为密钥 属性,无需配置 and/or 数据注释。此外,我希望此人的 Id 属性 是...好吧,此人的 ID。无需指定。
  • 我坚持,为了让你的生活更轻松,忘记在视图中使用你的实体类型,或者更糟糕的是,作为 Json/Xml return 类型。您应该为您的视图使用视图模型,为您的 Json/Xml return 类型使用数据传输对象 (DTO)。即使这个概念似乎违背了 KISS 原则,请相信我,事实并非如此。随着时间的推移,它只会简化你的生活,尽管一开始它可能看起来是毫无意义的麻烦。事实上,您的实体类型可能会随着时间的推移而演变,而您的视图可能不会,反之亦然。此外,您的视图通常需要比您的实体类型更多或不同的信息,并且沿着这条路走下去,您可能会发现自己开始通过调用等来探索您的视图,而它应该只包含视图逻辑。应在您的控制器操作中进行调用,并且这些操作应使用您的视图所需的所有 data/info 填充模型。