【问题标题】:Binding arrays in ASP.NET MVC without index?在没有索引的 ASP.NET MVC 中绑定数组?
【发布时间】:2014-02-06 23:07:07
【问题描述】:

我有一个如下所示的 HTML:

<input type="text" name="data[]" value="George"/>
<input type="text" name="data[]" value="John"/>
<input type="text" name="data[]" value="Paul"/>
<input type="text" name="data[]" value="Ringo"/>

在 PHP 中,我可以像这样接收这个数组:

$array = $_POST['name'];
// $array[0] == "George"

在 ASP.NET MVC 中,模型绑定器约定强制我在 HTML 中放置索引,因此控制器可以接收数组。

<!-- HTML for the ASP.NET MVC Version -->
<input type="text" name="data[0]" value="George"/>
<input type="text" name="data[1]" value="John"/>
<input type="text" name="data[2]" value="Paul"/>
<input type="text" name="data[3]" value="Ringo"/>

// C# Controller
public ActionResult SomeAction(string[] data)
{
    // Do stuff
}

如果我发送第一个 HTML,Action 中的数据将是 null

好吧,我认为这很糟糕。

如果我使用客户端代码来删除或添加项目到数组,我必须编写代码来重新索引 HTML 数组。

有没有办法扩展 ASP.NET MVC ModelBinder 以绑定没有索引的数组或解决此问题的解决方法?

编辑

在尝试了您的答案后,我得出的结论是,我发布的示例对我的目的没有用。我的真实情况是这样的:

查看

<table>
@for (var i = 0; i < Model.Sections.Count; ++i)
{
    <tr>
        <td><a href="#" class="edit-section"><span class="glyphicon glyphicon-question-sign"></span></a></td>
        <td>@Html.TextBoxFor(m => Model.Sections[i].SectionOrder, new { @class = "form-control" })</td>
        <td>@Html.TextBoxFor(m => Model.Sections[i].Title, new { @class = "form-control" })</td>
        <td>@Html.TextBoxFor(m => Model.Sections[i].SubTitle, new { @class = "form-control" })</td>
        <td>
            @Html.HiddenFor(m => Model.Sections[i].Id)
            <a href="#" class="delete-section"><span class="glyphicon glyphicon-remove"></span></a>
        </td>
    </tr>
}
</table>

动作

public ActionResult SaveSections(ICollection<SectionModel> sections)
{
    // DO STUFF
}

我尝试过手动输入 HTML,例如:

@for (var i = 0; i < Model.Sections.Count; ++i)
{
    <tr>
        <td><a href="#" class="edit-section"><span class="glyphicon glyphicon-question-sign"></span></a></td>
        <td>@Html.TextBox("Sections.SectionOrder", Model.Sections[i].SectionOrder, new { @class = "form-control" })</td>
        <td>@Html.TextBox("Sections.Title", Model.Sections[i].Title, new { @class = "form-control" })</td>
        <td>@Html.TextBox("Sections.SubTitle", Model.Sections[i].SubTitle, new { @class = "form-control" })</td>
        <td>
            @Html.Hidden("Sections.SubTitle", Model.Sections[i].Id)
            <a href="#" class="delete-section"><span class="glyphicon glyphicon-remove"></span></a>
        </td>
    </tr>
}

但它没有工作......

【问题讨论】:

  • 如果所有人的名称都相同,没有括号,那应该可以。尝试使用name="data" 对所有输入进行第一个html。
  • 查看Html.BeingCollectionItem。它很好地解决了这个问题。有一个谷歌的例子,但它运作良好。 Here's an old post 来自编写它的人,但您会通过几次搜索找到大量使用它的示例。

标签: html asp.net-mvc


【解决方案1】:

您不需要显式索引平面数据。如果你认为你有

<input type='text' name='data' value='George' />
<input type='text' name='data' value='John' />
<input type='text' name='data' value='Paul' />
<input type='text' name='data' value='Ringo' />

然后在你的控制器中你可以使用

public ActionResult Create(string[] data)
{
    //data should now be a string array of 4 elements
    return RedirectToAction("Index");
}

要了解活页夹,基本上是向后工作。当您提交表单时,假设它正在发布到您的 Create 方法,模型绑定器会检查方法参数。它会看到你有一个字符串数组作为参数,它被称为data。它喜欢字符串,因为表单数据是作为字符串提交的。除了在表单集合中查找键为data 的元素外,它不需要做任何实际工作。所有匹配的项目都将添加到数组中并分配给您的参数。

之所以有效,是因为参数与表单元素具有同名。如果名称不匹配,您将得到 null,因为没有找到具有该键名的任何内容。

如果您使用强视图(具有显式模型的视图),那么您可以使用 MVC 助手为您生成这些视图,并且输入元素将被分配正确的名称以映射回您的对象。

例如,如果您有一个模型:

public class BandMembers
{
    public string[] data {get; set;}
}

在您看来,您将其指定为模型并使用了适当的 HTML 助手,您的操作方法可能如下所示:

public ActionResult Create(BandMembers band)
{
    //band now has a property called 'data' with 4 elements
    return RedirectToAction("Index");
}

这应该会产生一个名为band 的实例化对象,它的属性names 有4 个值。这是因为模型绑定器看到一个名为 band 的参数与表单集合中的任何已知键都不匹配,意识到它是一个复杂对象(不是字符串、int、string[]、int[] 等)并检查它的成员。它看到这个对象有一个名为 data 的字符串数组,并且表单集合中有具有该名称的键。它收集值,将它们分配给 data 属性并将该对象分配给您的带参数。

现在您了解视图模型了!

*如果您在控制器中使用了 BandMembers 类但将其命名为data,则会收到警告,您将得到一个空值。这是因为模型绑定器在表单集合中找到键为 data 的项目,但无法弄清楚如何将它们从字符串转换为 BandMembers 对象。

编辑

关于您通过更深入的示例进行的编辑,这就是我想出的。

首先,我的模型是为了让我们在同一页面上。我创建了一个 FormData 对象,其中包含一个部分列表以充当对象的集合。

public class FormData
{
    public List<Section> Sections { get; set; }

    public FormData()
    {
    }
}

还有我的 Section.cs 类:

public class Section
{
    public bool IsDeleted { get; set; }
    public bool IsNew { get; set; }
    public int Id { get; set; }
    public int SectionOrder { get; set; }
    public string Title { get; set; }
    public string SubTitle { get; set; }

    public Section()
    {
    }
}

在您的 Section 上使用 EditorTemplate 可以轻松呈现带有为您生成的索引的内容。我自己模拟了这个项目并验证了它是有效的。不幸的是,正如您所看到的,一旦您删除了一个项目,您的索引就会出现故障。那么你如何解决这个问题?当然,您可以去阅读索引并重写它们,或者 - 只是不要删除它们!我在模拟项目中所做的是在 Section 上添加一个名为 IsDeleted 的新属性并将其呈现为隐藏元素。在删除单击的 JavaScript 处理程序中,我隐藏了该行并将该行的 IsDeleted 输入的隐藏输入更新为“true”。当我提交表单时,我现在将拥有一个完整的集合以及一个方便的标志,让我知道我需要从我的模型中删除哪些行。

我创建了一个测试视图,该视图绑定到一个名为 FormData 的模型,其中包含一个列表。

@model MVCEditorTemplateDemo.Models.FormData
@using (Html.BeginForm())
{
    <table id="section-container">
        @Html.EditorFor(m => m.Sections)
    </table>

    @Ajax.ActionLink("Add Section", "GetNewSection", "Home", new AjaxOptions() { HttpMethod="POST", InsertionMode=InsertionMode.InsertAfter, UpdateTargetId="section-container" })
    <input type="submit" value="Submit" />
}

是的,EditorFor 正在收集收藏!但是它怎么知道如何处理它呢?我在我的 Views/Home 中创建了一个名为 EditorTemplates 的文件夹(如果你想跨控制器使用它可以在 Shared 中),我在其中放置了一个名为 Section.cshtml 的局部视图。名称很重要——它应该与将要渲染的对象的名称相匹配。由于我的模型包含名为 Section 的对象,因此我的 EditorTemplate 也应称为 Section。

这是它的样子(EditorTemplates\Section.cshtml):

@model MVCEditorTemplateDemo.Models.Section
<tr>
    <td><a href="#" class="edit-section"><span class="glyphicon glyphicon-question-sign"></span></a></td>
    <td>@Html.TextBoxFor(m => Model.SectionOrder, new { @class = "form-control" })</td>
    <td>@Html.TextBoxFor(m => Model.Title, new { @class = "form-control" })</td>
    <td>@Html.TextBoxFor(m => Model.SubTitle, new { @class = "form-control" })</td>
    <td>
        @Html.HiddenFor(m => Model.Id)
        @Html.HiddenFor(m => Model.IsNew)
        @Html.HiddenFor(m => Model.IsDeleted)
        <a href="#" class="delete-section"><span class="glyphicon glyphicon-remove"></span></a>
    </td>
</tr>

为了满足您的要求,我尽量保持它与您所拥有的尽可能接近。当您计划动态添加或远程元素时,我通常不会使用表格。一些浏览器的表现不太好,更不用说我的设计师对渲染导致的并发症表有强烈的仇恨。

好的,现在您已经有了让 ASP.NET MVC 自动呈现您的项目并自动生成索引所需的东西。那么让我们看看如何删除该行。

回到我的测试视图,我添加了一个脚本部分,如下所示:

@section scripts
{
    <script type="text/javascript">
        $(function () { 
            $("table").on("click", ".delete-section", function() {
                $(this).closest('tr').hide();
                $(this).prev('input').val('true');
            });
        });
    </script>
}

这非常有效。当用户单击删除按钮时,他们会立即获得 UI 反馈,表明该行已消失,当提交表单时,我将使用一个方便的属性来呈现我呈现的整个项目集合,让我知道我需要从中删除哪些项目我的数据存储。最好的部分是我不必迭代我的集合,我的所有索引都是自动生成的。

我对你的问题的回答到此结束。

但后来我很好奇我需要做什么才能创建新行。所以让我们回顾一下Test 视图和Ajax.Action 助手。您会立即注意到我正在指示浏览器执行 POST 请求。为什么?因为浏览器可以缓存 GET 请求以优化性能。通常您不会在意,因为您通常会为每个请求返回相同的 HTML,但由于我们需要包含特殊命名,因此我们的 HTML 实际上每次都是不同的(在我们的输入名称中包含索引)。其余的都是不言自明的。真正的诀窍是在服务器端——我们如何返回一个部分以通过正确的索引向该表添加一行?

不幸的是,该框架虽然擅长正常查找视图,但似乎在检查 EditorTemplates 和 DisplayTemplates 文件夹时失败了。因此,如果我们不使用模板,我们的 Action 会比通常情况下更脏。

public ActionResult GetNewSection()
{
    var section = new Section() { IsNew = true };
    FakedData.Sections.Add(section);
    ViewData.TemplateInfo.HtmlFieldPrefix = string.Format("Sections[{0}]", FakedData.Sections.Count-1);
    return PartialView("~/Views/Home/EditorTemplates/Section.cshtml", section);
}

好的,那么我们看到了什么?首先,我创建了一个新的 Section 对象,因为我需要它来渲染 EditorTemplate。我添加了第二个新属性 IsNew,但目前我实际上并没有对它做任何事情。我只是想要一种方便的方式来查看即将在我的 POST 方法中添加和删除的内容。

我将该新部分添加到我的数据存储 (FakedData)。您可以改为以另一种方式跟踪新请求的数量 - 只需确保每次单击“添加部分”链接时它都会增加。

现在是诀窍。由于我们要返回一个局部视图,它没有父模型的上下文。如果我们只返回只传入部分的模板,它就不会知道它是更大集合的一部分,也不会适当地生成名称。所以我们用 HtmlFieldPrefix 字段告诉它我们在哪里。我使用我的数据存储来跟踪正确的索引,但同样,这可能来自任何地方,如果您添加了 IsNew 属性,您将能够在提交时将新的(和未删除的)记录添加到您的存储中.

就像我说的那样,框架仅根据名称查找模板有点麻烦,因此我们必须给出它的路径才能正确返回。总体上没什么大不了的,但有点烦人。

现在我为 Test 视图添加了一个 POST 处理程序,并且我可靠地获得了删除、添加和总计数的项目计数。请记住,行可以是新的和已删除的!

[HttpPost]
public ActionResult Test(FormData form)
{
    var sectionCount = form.Sections.Count();
    var deletedCount = form.Sections.Count(i => i.IsDeleted);
    var newItemCount = form.Sections.Count(i => i.IsNew);

    form.Sections = form.Sections.Where(s => !s.IsDeleted).ToList();
    FakedData = form;
    return RedirectToAction("Test");
}

就是这样。我们有一个完整的端到端渲染你的集合与适当的索引,“删除”行,添加新行,我们不必破解模型绑定器,操纵名称或诉诸 JavaScript 黑客来重新编号我们提交的元素。

我渴望对此提供反馈。如果我没有看到更好的答案,这可能是我以后做此类事情时总是使用的路线。

【讨论】:

  • 感谢您的有用解释,我会尝试删除括号,我的收藏实际上比示例更复杂。
  • 我已经发布并编辑,显示我的问题与真实情况
  • 已编辑以包括使用 EditorTemplates 为您自动生成索引以及有关使用 IsDeleted 标志而不是 ajaxically 删除您打算在表单中提交的项目的提示。然后我被带走了,也添加了新的行。代码只有几十行,所以不是很复杂。希望您发现它有用并且可以满足您的需求。接受反馈并进一步完善问题和答案。
  • 感谢@Erik 提供广泛而详细的回答!
  • 我最近一直在做更多的 ASP.NET MVC 东西,所以看看如何做是一个很好的挑战,以供我自己参考。 ASP.NET MVC 问题不会产生太多的声誉点,但它是一个很好的转移!我希望这能解决您的问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-03-01
  • 2013-05-27
  • 2011-01-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多