MVC 4 - 多对多关系和复选框

9
我正在使用ASP.NET MVC 4和Entity Framework。在我的数据库中,我有一个名为Subscription的表,表示对公共交通的订阅。该订阅可以提供对几个公共交通公司的访问(因此一个订阅可以拥有1、2、3等多个公司),因此这些表之间存在多对多关系(它们之间有一个中间表)。
我想通过一个包含订阅金额字段Amount和可用公司的复选框的页面来允许创建订阅。每个复选框代表一个现有公司(存储在我的数据库中)。
有什么想法吗?我已经阅读了这篇ASP.NET MVC Multiple Checkboxes,但并没有真正帮助到我。
编辑:这是我的表图。

你可以像这样拥有一个下拉列表:链接 - Tiago Almeida
不,我不是在寻找下拉列表,因为正如我所说,一个订阅可以由2个或更多公司组成。所以我认为复选框更好。 - Traffy
我不知道你在使用哪个视图引擎,但这并不难,你只需要迭代公司并创建按钮。你可能需要更改模型类。 - Tiago Almeida
我正在使用 Razor 引擎。创建公司并不难,但是例如“创建”操作怎么样呢?我该如何检查哪些复选框被选中了,哪些没有? - Traffy
尝试使用视图模型来实现,但我不知道如何获取它。 - Traffy
显示剩余2条评论
3个回答

20

你开始时有两个视图模型。第一个代表所选公司的详细信息,例如名称、地址和行业类型。第二个视图模型是用于提交用户评论的表单,其中包含用户名、评论文本和评分。

public class CompanySelectViewModel
{
    public int CompanyId { get; set; }
    public string Name { get; set; }
    public bool IsSelected { get; set; }
}

...第二个用于创建订阅:

public class SubscriptionCreateViewModel
{
    public int Amount { get; set; }
    public IEnumerable<CompanySelectViewModel> Companies { get; set; }
}

然后在 SubscriptionController 的 GET 操作中,您从数据库加载公司以初始化视图模型:

public ActionResult Create()
{
    var viewModel = new SubscriptionCreateViewModel
    {
        Companies = _context.Companies
            .Select(c => new CompanySelectViewModel
            {
                CompanyId = c.CompanyId,
                Name = c.Name,
                IsSelected = false
            })
            .ToList()
    };

    return View(viewModel);
}

现在,您为此操作拥有了强类型视图:
@model SubscriptionCreateViewModel

@using (Html.BeginForm()) {

    @Html.EditorFor(model => model.Amount)

    @Html.EditorFor(model => model.Companies)

    <input type="submit" value="Create" />
    @Html.ActionLink("Cancel", "Index")
}

为了正确呈现公司的复选框,您需要引入一个编辑器模板。它必须具有名称CompanySelectViewModel.cshtml,并放置在文件夹Views/Subscription/EditorTemplates中(如果不存在,请手动创建此文件夹)。这是一个强类型的局部视图:
@model CompanySelectViewModel

@Html.HiddenFor(model => model.CompanyId)
@Html.HiddenFor(model => model.Name)

@Html.LabelFor(model => model.IsSelected, Model.Name)
@Html.EditorFor(model => model.IsSelected)

名称作为隐藏字段添加,以便在POST期间保留名称。

显然需要对视图进行更多的样式设置。

现在,您的POST操作将如下所示:

[HttpPost]
public ActionResult Create(SubscriptionCreateViewModel viewModel)
{
    if (ModelState.IsValid)
    {
        var subscription = new Subscription
        {
            Amount = viewModel.Amount,
            Companies = new List<Company>()
        };

        foreach (var selectedCompany
            in viewModel.Companies.Where(c => c.IsSelected))
        {
            var company = new Company { CompanyId = selectedCompany.CompanyId };
            _context.Companies.Attach(company);

            subscription.Companies.Add(company);
        }

        _context.Subscriptions.Add(subscription);
        _context.SaveChanges();

        return RedirectToAction("Index");
    }

    return View(viewModel);
}

您可以使用Attach,也可以先使用var company = _context.Companies.Find(selectedCompany.CompanyId);加载公司。但是使用Attach可以避免到数据库中加载要添加到集合中的公司的往返。

(编辑2:此答案中,为相同示例模型的Edit操作和视图提供了续篇。)

编辑

您的模型实际上不是多对多关系,而是两个一对多关系。通常情况下,不需要PublicTransportSubscriptionByCompany实体。如果在该表中有由Id_PublicTransportSubscription、Id_PublicTransportCompany组成的复合主键,并删除id列Id_PublicTransportSubscriptionByCompanyId,EF将检测到此表模式作为多对多关系并在每个实体中创建一个订阅和公司的集合,而不会为链接表创建任何实体。然后就可以应用我上面的代码。

如果出于某种原因不想更改方案,则必须像以下方式更改POST操作:

[HttpPost]
public ActionResult Create(SubscriptionCreateViewModel viewModel)
{
    if (ModelState.IsValid)
    {
        var subscription = new Subscription
        {
            Amount = viewModel.Amount,
            SubscriptionByCompanies = new List<SubscriptionByCompany>()
        };

        foreach (var selectedCompany
            in viewModel.Companies.Where(c => c.IsSelected))
        {
            var company = new Company { CompanyId = selectedCompany.CompanyId };
            _context.Companies.Attach(company);

            var subscriptionByCompany = new SubscriptionByCompany
            {
                Company = company
            };

            subscription.SubscriptionByCompanies.Add(subscriptionByCompany);
        }

        _context.Subscriptions.Add(subscription);
        _context.SaveChanges();

        return RedirectToAction("Index");
    }

    return View(viewModel);
}

非常感谢,我认为这会有所帮助!我现在会尝试并告诉您。不过,有一个小问题:使用您的示例,部分视图的呈现是否会自动完成?再次感谢。 - Traffy
1
@Traffy:是的,EditorFor(model => model.Companies) 检测到 Companies 是一个集合,并为每个项目呈现一个局部视图。模板文件的名称(必须匹配模型名称)和路径必须精确,因为MVC会按照某些约定搜索局部视图(通常在 Views/{ControllerName}Shared 中,但在名为 EditorTemplates 的子文件夹中)。 - Slauma
@Traffy:哦,亲爱的,你在使用这个过时的EF东西,还有EntityObject而没有POCOs吗?你可以尝试注释掉那一行。我相信EntityObject会自动初始化其导航集合。 - Slauma
2
@Traffy:这需要更多的工作。您将拥有类似的视图模型(但包括SubscriptionEditViewModel中的Id),加载订阅,包括已关联公司的Ids,再次加载所有公司,但然后为已经与订阅相关的公司设置IsSelectedtrue。POST操作更加困难,因为现在当用户取消选中一个公司时,您需要从链接表中删除实体,并在用户选中新公司时向链接表添加实体。只要开始并在遇到问题时提出新问题即可 :) - Slauma
@MattFlowers:有人在新问题中问了同样的问题。答案在这里:https://dev59.com/pHTYa4cB1Zd3GeqPy8s6#17819169 - Slauma
显示剩余6条评论

1

这只是对Slauma回答的一个扩展。在我的情况下,我需要表现多对多关系,就像产品和角色之间的表格一样,第一列代表产品,标题代表角色,表格用复选框填充以选择产品的角色。 为了实现这个目标,我使用了像Slauma描述的ViewModel,但添加了另一个包含最后两个模型的模型,如下所示:

public class UserViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public IEnumerable<ProductViewModel> Products { get; set; }
}

public class ProductViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public IEnumerable<RoleViewModel> Roles { get; set; } 
}
public class RoleViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsSelected { get; set; }
}

接下来,在控制器中我们需要填充数据:
UserViewModel user = new UserViewModel();
user.Name = "Me";
user.Products = new List<ProductViewModel>
                {
                    new ProductViewModel
                    {
                        Id = 1,
                        Name = "Prod1",
                        Roles = new List<RoleViewModel>
                        {
                            new RoleViewModel
                            {
                                Id = 1,
                                Name = "Role1",
                                IsSelected = false
                            }
                            // add more roles
                        }
                    }
                    // add more products with the same roles as Prod1 has
                 };

下一步,在视图中:

@model UserViewModel@using (Ajax.BeginForm("Create", "User",
new AjaxOptions
{
    HttpMethod = "POST",
    InsertionMode = InsertionMode.Replace,
    UpdateTargetId = "divContainer"
}))
{
<table>
    <thead>
        <tr>
            <th>
            </th>
            @foreach (RoleViewModel role in Model.Products.First().Roles.ToList())
            {
                <th>
                    @role.Name
                </th>
            }
        </tr>
    </thead>
    <tbody>
        @Html.EditorFor(model => model.Products)
    </tbody>
</table>
<input type="submit" name="Create" value="Create"/>
}

正如您所看到的,EditorFor正在使用产品模板:

@model Insurance.Admin.Models.ProductViewModel
@Html.HiddenFor(model => model.Id)
<tr>
    <th class="col-md-2 row-header">
        @Model.Name
    </th>
    @Html.EditorFor(model => model.Roles)
</tr>

这个模板使用另一个角色模板:

@model Insurance.Admin.Models.RoleViewModel
@Html.HiddenFor(model => model.Id)
<td>
    @Html.EditorFor(model => model.IsSelected)
</td>

看,我们有一个表格,第一列是产品,标题是角色,表格中填充了复选框。我们正在发布UserViewModel,您将看到所有数据都已发布。


1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接