MVC 4 EF - 产品/多个类别

MVC 4 EF - Product / Multiple Categories

编辑:我想我需要某种视图模型,但我不确定如何处理这种关系。

我试图首先理解 MVC 4 和 EF 代码,并且试图映射多对多关系。

我有两个类;

public class Asset
{

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

    public virtual ICollection<Category> Categories { get; set; }
}

public class Category
{
    public int CategoryId { get; set; }
    public string CategoryName { get; set; }
    public string Description { get; set; }

    public virtual ICollection<Asset> Assets { get; set; }
}

所以我试图让每个资产有多个类别,每个类别可能有多个资产。

在我的创建方法上;

    public ActionResult Create()
    {
        var model = new Asset();

        model.Categories = _db.Categories.ToList();
        return View(model);
    }

在我看来,我可以显示这些类别的唯一方法是说; (注意 Model 中的大写 M。我不能使用视图中其他地方使用的小写模型)

@model MyProject.Models.Asset
    @using (Html.BeginForm("Create", "Assets", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
    <div>
        @foreach (var item in Model.Categories)
        {
            <p>@item.CategoryName</p>
        }

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

调用初始创建时,我可以看到我的资产并且它有类别。然而,在 return 创建方法中,它是空的。我不知道为什么。我知道我没有做任何事情来编辑视图中的这些类别,我做不到那么远。但我不明白的是为什么我的模型留下类别,但返回 none。

我的创建return(这里我的资产类别是空的)

    // POST: Assets/Create
    [HttpPost]
    public ActionResult Create(Asset model)
    {
        if (!ModelState.IsValid)
        {
            //error, return to view.
            return View();
        }
        try
        {
             //do stuff
        }
        catch
        {
            return View();
        }
    }

最终,在创建资产时,我希望能够列出所有类别,并允许对该新资产所属的类别进行一些选择。如果有人能帮我解决这个问题,你就是我的英雄。但如果我能理解为什么返回的不是我发出的,那将是一个开始。

1. return 查看();

您没有在创建方法中传回模型,这就是您看不到模型为 NULL 的原因。

    // POST: Assets/Create
    [HttpPost]
    public ActionResult Create(Asset model)
    {
        if (!ModelState.IsValid)
        {
            //error, return to view.
            return View(model);
// If you don't pass back the model to you view you will see model is NULL
        }
        try
        {
             //do stuff
        }
        catch
        {
            return View(model);
        }
    }
  1. 在您的情况下,类别将始终为空,因为您不能像在您的情况下那样post支持列表。

尝试在循环中显示它们,然后 MVC 模型绑定器将能够将您的列表绑定到模型:

@for (int i = 0; i < Model.Categories; i++)
{
    @Html.HiddenFor(model => model.Categories[i].CategoryId)
}
  1. 如果要保存 SelectedCategories,则必须使用 MultiSelect

您的类别为空的原因是您没有将其绑定到 POST。 POST.

期间他们不在字段中

试试这个,看看它们是否已填写:

@for (int i = 0; i < Model.Categories; i++)
{
    @Html.TextBoxFor(model => model.Categories[i].CategoryId)
    @Html.TextBoxFor(model => model.Categories[i].CategoryName)
}

In my view, the only way I can show these categories is to say; (Note the capital M in Model. I can't use the lower case model as used elsewhere in the view)

我一直讨厌 Microsoft 通过其生成的视图、教程和在线文章使用 model => model.* 约定;它只会导致混乱。在您看来 Model 是一个实际的对象实例,即您为视图定义为 "model" 的实例。您在 Html.EditorFor 之类的东西中看到的小写 model 实际上是 lambda 表达式的参数。可以叫随便。例如,Html.EditorFor(x => x.Foo) 甚至 Html.EditorFor(supercalifragilisticexpialidocious => supercalifragilisticexpialidocious.Foo) 也同样有效。尽管传递给该参数的值通常是 Model 对象实例,但是 Modelmodel 是完全不同的概念。

When the initial create is called, I can see my asset and it has categories. On the return create method however, its null. I can't work out why. I understand I'm not doing anything to edit these categories in the View, I can't get that far. What I don't understand though is why my model leaves with categories, but comes back with none.

这就是原因。您没有做任何事情来编辑视图中的这些类别。没有字段供它们 post 与表单数据一起编辑,因此,在您的操作中由模型绑定程序实例化的 class 不包含任何类别数据。这是关键。进入视图的 class 实例 不是 在类似 post 之后返回的相同 class 实例。每一个都是独一无二的东西。 post 动作不知道它之前发生的任何事情;它只是包含 posted 的任何数据。假设该操作采用特定 class 的参数,模型绑定器将尝试新建该 class 的实例并将 posted 数据绑定到 class 上的适当属性].它不关心最初发送给视图的是什么;它甚至不关心 class 它正在处理什么。

Ultimately when creating an Asset, I want to be able to list all the categories and allow some selection as to which categories this new asset will belong. If someone could help me work that out, you're my hero. But if I could just understand why what's coming back isn't what I sent out, that would be a start.

这是有趣的部分。首先,您绝对必须为此使用视图模型。如果您不熟悉视图模型,它们只是用作视图模型的 classes,因此得名。您在这里传递的 Asset,在技术上是一个 "entity",它是一个用于数据传输的 class,通常是 to/from 一个数据库。虽然实体可以用作视图的模型,正如您在此处所做的那样,但它并不真正适合这种情况。

存在明显的利益冲突,因为 class 表示数据库中某些 table 模式的需求与 class 表示数据库中数据的需求大不相同UI层。这就是视图模型的用武之地。在最传统的意义上,视图模型只是表示需要在一个或多个视图中显示 and/or 编辑的数据。它可能与特定实体有许多共同的属性,或者它可能只有这些属性的一个子集,甚至是完全不同的属性。你的应用程序的工作是 "map" 从你的实体到你的视图模型,反之亦然,这样将实体保存到持久性存储的逻辑可以完全从用户如何交互的逻辑中抽象出来那个数据。

视图模型对您的目的如此重要的原因是 HTML 中的表单元素有一定的局限性。它们只能处理可以表示为字符串的数据:例如整数、布尔值、实际字符串等。它们特别不适合处理复杂对象,例如您的 Category class。换句话说,post 返回代表 Category 的整数 id 列表是完全可以实现的,但是 post 返回完整的 Category 实例是完全不可信的已被用户选择。

由于您的实体需要一个类别列表,而您的视图可能只能够 posting 一个整数列表,所以存在根本的脱节。使用视图模型提供了一种弥合差距的方法。此外,它还允许您拥有其他属性,例如用于填充 select 列表的类别选择列表,这完全不适合放在您的实体 class.

对于您的场景,您需要一个像这样的视图模型:

public class AssetViewModel
{
    // any other asset properties you need to edit

    public List<int> SelectedCategoryIds { get; set; }

    public IEnumerable<SelectListItem> CategoryChoices { get; set; }
}

然后您可以使用以下方法在您的视图中创建一个多select列表:

@Html.ListBoxFor(m => m.SelectedCategoryIds, Model.CategoryChoices)

现在,用实体中的数据填充视图模型。在创建视图中,实体尚不存在,因此您无需进行任何映射。您唯一需要做的就是填充 CategoryChoices 属性,以便视图中的 select 列表包含一些数据。但是,基于上面关于需要 posted 返回数据的讨论,否则它将为空,因为 select 列表的实际内容永远不会被 posted,你您需要在每个创建和编辑操作中填充它,包括 GET 和 POST。因此,最好将此逻辑分解为控制器上每个操作都可以调用的私有方法:

private void PopulateCategoryChoices(AssetViewModel model)
{
    model.CategoryChoices = db.Categories.Select(m => new SelectListItem
    {
        Value = m.Id,
        Text = m.Name
    };
}

然后,在您的创建 GET 操作中,您只需新建您的视图模型并填充您的类别选择:

public ActionResult Create()
{
    var model = new AssetViewModel();

    PopulateCategoryChoices(model);
    return View(model);
}

在 post 版本中,您现在需要将 posted 数据映射到您的 Asset 实体:

[HttpPost]
public ActionResult Create(AssetViewModel model)
{
    if (ModelState.IsValid)
    {
        var asset = new Asset
        {
            Title = model.Title,
            Description = model.Description,
            // etc.
            Categories = db.Categories.Where(m => model.SelectedCategoryIds.Contains(m.Id))
        }

        db.Assets.Add(asset);
        db.SaveChanges();
        return RedirectToAction("Index");
    }

    PopulateCategoryChoices(model);
    return View(model);
}

编辑 GET 操作类似于创建版本,只是这一次,您有一个现有实体需要映射到视图模型的实例:

var asset = db.Assets.Find(id);
if (asset == null)
{
    return new HttpNotFoundResult();
}

var model = new AssetViewModel
{
    Title = asset.Title,
    Description = asset.Description,
    // etc.
    SelectedCategoryIds = asset.Categories.Select(m => m.Id).ToList()
};

同样,编辑 POST 操作类似于创建版本,但您要从视图模型映射到现有资产,而不是创建新资产。此外,由于存在多对多关系,因此在保存类别时必须格外小心。

// map data
asset.Title = model.Title;
asset.Description = model.Description;
//etc.

// You might be tempted to do the following:
// asset.Categories = db.Categories.Where(m => model.SelectedCategoryIds.Contains(m.Id));

// Instead you must first, remove any categories that the user deselected:
asset.Categories.Where(m => !model.SelectedCategoryIds.Contains(m.Id))
    .ToList().ForEach(m => asset.Categories.Remove(m));

// Then you need to add any newly selected categories
var existingCategories = asset.Categories.Select(m => m.Id).ToList();
db.Categories.Where(m => model.SelectedCategoryIds.Except(existingCategories).Contains(m.Id))
    .ToList().ForEach(m => asset.Categories.Add(m));

这里的额外步法是必要的,以防止两次保存相同的关系,从而导致完整性错误。默认情况下 Entity Framework 为多对多关系创建一个联接 table,该关系由一个复合主键组成,该主键由关系每一侧的外键组成。