MVC提交复杂对象列表

20

我有一个 FeedbackViewModel,其中包含一组问题:

public class FeedbackViewModel
{
    public List<QuestionViewModel> Questions { get; set; }
}

QuestionViewModel是一个可以被5种不同类型的问题继承的对象。

public class QuestionViewModel
{
    public string QuestionText { get; set; }
    public string QuestionType { get; set; }
}

继承一个问题类型的示例:

public class SingleQuestionViewModel : QuestionViewModel
{
    public string AnswerText { get; set; }
}

在控制器的Index动作的HttpGet中,我从数据库中获取问题,并将正确的问题类型添加到FeedbackViewModel中的问题列表中。然后我在视图中呈现这个模型:

@using (Html.BeginForm())
{
    //foreach (var item in Model.Questions)
    for (int i = 0; i < Model.Questions.Count; i++)
    {
        <div class="form-group">
            @Html.DisplayFor(modelItem => Model.Questions[i].QuestionText, new { @class = "control-label col-md-4" })
            <div class="col-md-6">
                @if (Model.Questions[i].QuestionType == "Single")
                {
                    @Html.EditorFor(modelItem => (Model.Questions[i] as OpenDataPortal.ViewModels.SingleQuestionViewModel).AnswerText)
                }
                else if (Model.Questions[i].QuestionType == "Multiple")
                {
                    @Html.TextAreaFor(modelItem => (Model.Questions[i] as OpenDataPortal.ViewModels.SingleQuestionViewModel).AnswerText)
                }
                else if (Model.Questions[i].QuestionType == "SingleSelection")
                {
                    @Html.RadioButtonForSelectList(modelItem => (Model.Questions[i] as OpenDataPortal.ViewModels.SingleSelectionQuestionViewModel).SelectedAnswer,
                                                                (Model.Questions[i] as OpenDataPortal.ViewModels.SingleSelectionQuestionViewModel).SelectionAnswers)
                }
                else if (Model.Questions[i].QuestionType == "MultipleSelection")
                {
                    @Html.CustomCheckBoxList((Model.Questions[i] as OpenDataPortal.ViewModels.MultipleSelectionQuestionViewModel).AvailableAnswers)
                }
                else if (Model.Questions[i].QuestionType == "UrlReferrer")
                {
                    @Html.EditorFor(modelItem => (Model.Questions[i] as OpenDataPortal.ViewModels.SingleQuestionViewModel).AnswerText)
                }
            </div>
        </div>
        <br />
    }

    <br />
    <button type="submit">Submit</button>
}

在这里输入图像描述


现在,我无法让模型发布问题列表。发布不同对象类型的列表可能吗?


编辑:以下是我使用 Fiddler 发现在帖子中的数据列表:

在这里输入图像描述


我看不出为什么它不能工作。只要输入的名称正确设置,您应该能够接收数据。但是,您的问题可能来自于默认模型绑定器。您的复杂类型可能对其技能过于复杂。您应该首先检查名称是否正确设置,然后检查发布到服务器的数据并编写自己的模型绑定器。 - Andrei V
@AndreiV - 我使用 Fiddler 检查了从页面发布的名称和值,并将其添加到上面的问题中。这些值似乎都在那里。你认为我需要编写一个模型绑定器吗?(我以前从未这样做过) - Carel
1
没有看到确切的POST数组,我只是猜测:如果您正在尝试对任何类型的集合进行模型绑定,则索引不能跳过数字,否则模型绑定器会跳过之后的所有内容。因此,如果您的POST值类似于Questions[1].SelectedAnswer等,则默认的模型绑定器会出现问题。 - Tieson T.
我实际上是指HTML元素上设置的名称(带有相应的索引)。您应该首先检查HTML是否正确生成,然后再考虑自定义模型绑定器。我的回答对于这个问题可能会有所帮助。这个问题 - Andrei V
1
如果您的POST方法参数是FeedbackViewModel,那么您只会得到List<QuestionViewModel> Questions(基本类型),而不是SingleQuestionViewModelMultipleSelectionQuestionViewModel的实例。DefaultModelBinder无法知道您是否需要继承类型。这篇文章可能会给出一些创建自定义ModelBinder的线索,尽管为5种不同类型创建5个集合属性可能更容易。 - user3559349
最终,我选择将所有可能的不同问题类型放在一个类/模型中,并完全摒弃了继承。 - Carel
5个回答

35

经过大量研究,我找到了两个解决方案:

  1. 一种方法是编写具有硬编码ID和名称的HTML代码。
  2. 第二种方法是将ICollection/IEnumerable转换为Array或List(即IList),并在Controller POST操作中的BindingModel中包含一个Array对象。

感谢Phil Haack (@haacked) 2008年的博客文章http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/ 这篇文章对于MVC默认的ModelBinder如何工作仍然很相关。 (注意:Phil的文章中链接到示例项目和扩展方法的链接已失效)

启发我的HTML片段:

<form method="post" action="/Home/Create">
    <input type="hidden" name="products.Index" value="cold" />
    <input type="text" name="products[cold].Name" value="Beer" />
    <input type="text" name="products[cold].Price" value="7.32" />

    <input type="hidden" name="products.Index" value="123" />
    <input type="text" name="products[123].Name" value="Chips" />
    <input type="text" name="products[123].Price" value="2.23" />

    <input type="submit" />
</form>

POST数组大致如下:

products.Index=cold&products[cold].Name=Beer&products[cold].Price=7.32&products.Index=123&products[123].Name=Chips&products[123].Price=2.23

模型:

public class CreditorViewModel
{
    public CreditorViewModel()
    {
        this.Claims = new HashSet<CreditorClaimViewModel>();
    }
    [Key]
    public int CreditorId { get; set; }
    public string Comments { get; set; }
    public ICollection<CreditorClaimViewModel> Claims { get; set; }
    public CreditorClaimViewModel[] ClaimsArray { 
        get { return Claims.ToArray(); }
    }
}

public class CreditorClaimViewModel
{
    [Key]
    public int CreditorClaimId { get; set; }
    public string CreditorClaimType { get; set; }
    [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:N2}")]
    public Decimal ClaimedTotalAmount { get; set; }
}

控制器 GET:

public async Task<ActionResult> Edit(int id)
    {
        var testmodel = new CreditorViewModel
        {
            CreditorId = 1,
            Comments = "test",
            Claims = new HashSet<CreditorClaimViewModel>{
                new CreditorClaimViewModel{ CreditorClaimId=1, CreditorClaimType="1", ClaimedTotalAmount=0.00M},
                new CreditorClaimViewModel{ CreditorClaimId=2, CreditorClaimType="2", ClaimedTotalAmount=0.00M},
            }
        };
        return View(model);
    }

Edit.cshtml:

@Html.DisplayNameFor(m => m.Comments)
@Html.EditorFor(m => m.Comments)

<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(m => Model.Claims.FirstOrDefault().CreditorClaimType)
        </th>
        <th>
            @Html.DisplayNameFor(m => Model.Claims.FirstOrDefault().ClaimedTotalAmount)
        </th>
    </tr>        
<!--Option One-->
@foreach (var item in Model.Claims)
{
    var fieldPrefix = string.Format("{0}[{1}].", "Claims", item.CreditorClaimId);
    <tr>
        <td>
            @Html.DisplayFor(m => item.CreditorClaimType)
        </td>
        <td>
        @Html.TextBox(fieldPrefix + "ClaimedTotalAmount", item.ClaimedTotalAmount.ToString("F"),
        new
        {
            @class = "text-box single-line",
            data_val = "true",
            data_val_number = "The field ClaimedTotalAmount must be a number.",
            data_val_required = "The ClaimedTotalAmount field is required."
        })
        @Html.Hidden(name: "Claims.index", value: item.CreditorClaimId, htmlAttributes: null)
        @Html.Hidden(name: fieldPrefix + "CreditorClaimId", value: item.CreditorClaimId, htmlAttributes: null)
        </td>
    </tr>
    }
</table>    
<!--Option Two-->
@for (var itemCnt = 0; itemCnt < Model.ClaimsArray.Count(); itemCnt++)
{
    <tr>
        <td></td>
        <td>
            @Html.TextBoxFor(m => Model.ClaimsArray[itemCnt].ClaimedTotalAmount)
            @Html.HiddenFor(m => Model.ClaimsArray[itemCnt].CreditorClaimId)
    </td></tr>
}

表单在控制器中被处理:

Post模型:

public class CreditorPostViewModel
{
    public int CreditorId { get; set; }
    public string Comments { get; set; }
    public ICollection<CreditorClaimPostViewModel> Claims { get; set; }
    public CreditorClaimPostViewModel[] ClaimsArray  { get; set; }
}

public class CreditorClaimPostViewModel
{
    public int CreditorClaimId { get; set; }
    public Decimal ClaimedTotalAmount { get; set; }
}

控制器:

[HttpPost]
    public ActionResult Edit(int id, CreditorPostViewModel creditorVm)
    {
        //...

4

请确保按顺序渲染视图,以便Model.Questions[i]按顺序呈现。

例如,Model.Questions[0],Model.Questions[1],Model.Questions[2]。我注意到,如果顺序不正确,mvc模型绑定器只会绑定第一个元素。


1
这是作为答案发布的,但它并没有试图回答问题。它可能应该是一个编辑、评论、另一个问题或完全删除。 - user6613600

3

感谢您在此帖子中指导我正确的方向。我一直苦于绑定非连续的IDictionary<string, bool>对象的语法不正确。虽然不确定这是否百分百正确,但是以下Razor代码对我有效:

<input type="hidden" name="MyDictionary.Index" value="ABC" />
<input type="hidden" name="MyDictionary[ABC].Key" value="ABC" />
@Html.CheckBox(name: "MyDictionary[ABC].Value", isChecked: Model.MyDictionary["ABC"], htmlAttributes: null)

如果您需要复选框,确保使用 Html.CheckBox 而不是标准的 HTML 复选框。如果未提供值,模型将崩溃,并且 Html.CheckBox 会生成一个隐藏字段,在未选中复选框时确保有一个值存在。

1
使用Razor,您可以像下面这样使用字典实现for循环,而无需更改您的对象:
@foreach (var x in Model.Questions.Select((value,i)=>new { i, value }))
{
     if (Model.Questions[x.i].QuestionType == "Single")
     {
          @Html.EditorFor(modelItem => (modelItem.Questions[x.i] as OpenDataPortal.ViewModels.SingleQuestionViewModel).AnswerText)
     }
   ...
}

集合必须是List或Array才能使此代码正常工作。

-1

我使用这段代码,或许可以帮助到你

<input type="hidden" name="OffersCampaignDale[@(item.ID)].ID" value="@(item.ID)" />

@Html.Raw(Html.EditorFor(modelItem => item.NameDale, new { htmlAttributes = new { @class = "form-control" } })
.ToString().Replace("item.NameDale", "OffersCampaignDale[" + item.ID+ "].NameDale").Replace("item_NameDale", "NameDale-" + item.ID))
@Html.ValidationMessageFor(modelItem => item.NameDale, "", new { @class = "text-danger" })

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