MVC 5:模型中的字典绑定到视图中的一系列复选框?

MVC 5: dictionary in model bound to a series of checkboxes in view?

当我POST将我的模型保存到控制器时,控制器得到一个空字典。

哪里出了问题?绑定工作有什么特别的吗?

我的模型有这个 属性:

public Dictionary<int, bool> DictionaryTest { get; set; }

我的控制器在调用视图之前填充了一些数据:

mymodel.DictionaryTest = new Dictionary<int, bool> { { 0, false }, { 1, true }, { 2, false } };

我的视图使用以下代码正确显示它:

@Html.CheckBoxFor(m_ => m_.DictionaryTest[0], new { @class = "form-control" })
@Html.CheckBoxFor(m_ => m_.DictionaryTest[1], new { @class = "form-control" })
@Html.CheckBoxFor(m_ => m_.DictionaryTest[2], new { @class = "form-control" })

非常感谢

为了绑定字典,您需要有 post 个值,例如:DictionaryProperty[N].KeyDictionaryProperty[N].Value。因此,您的 Razor 代码需要类似于:

@Html.HiddenFor(m_ => m_.DictionaryTest[0].Key)
@Html.CheckBoxFor(m_ => m_.DictionaryTest[0].Value, new { @class = "form-control" })

@Html.HiddenFor(m_ => m_.DictionaryTest[1].Key)
@Html.CheckBoxFor(m_ => m_.DictionaryTest[1].Value, new { @class = "form-control" })

@Html.HiddenFor(m_ => m_.DictionaryTest[2].Key)
@Html.CheckBoxFor(m_ => m_.DictionaryTest[2].Value, new { @class = "form-control" })

但是,如果您的键是整数,那么字典就有点过分了。只需使用一个简单的列表。然后,您的 Razor 代码将按原样运行。

我想我应该在这里写点东西,因为我尝试了接受的答案并最终遇到了编译问题 - 我不了解你们,但是 DictionaryTest[index].Key 行就是不知道为我工作 - array-accessing 什么时候需要 Key 属性?!

对于其他正在寻找答案的人来说,有一些技巧可以使这一切像魔术一样工作。简而言之,这一切都取决于此:

为了将多个属性绑定到一个对象(可以是任何复杂类型的列表或字典),模型绑定器需要知道哪些属性属于一组 - 否则它会纠缠在一起。

假设您有这样的类型:

public class TestClass
{
    public string Prop1 { get; set; }
    public string Prop2 { get; set; }
}

并且您希望它绑定到这样的方法中:

public ActionResult MyAction(List<TestClass> test)
{
    ...
}

如果您像这样在页面上转储一堆输入:

<input name="Prop1" value="FirstP1" />
<input name="Prop2" value="FirstP2" />

<input name="Prop1" value="SecondP1" />
<input name="Prop2" value="SecondP2" />

模型绑定器如何知道哪些要放在一起?简短回答:它没有 - 它需要一点帮助。你可能已经知道,关键是给它相关的索引,这样它就可以找出要分组的索引:

<input name="test[0].Prop1" value="FirstP1" />
<input name="test[0].Prop2" value="FirstP2" />

<input name="test[1].Prop1" value="SecondP1" />
<input name="test[1].Prop2" value="SecondP2" />

现在模型绑定器可以将前两个 [0] 组合在一起,然后将后两个 [1] 组合在一起,瞧,我们有一个列表绑定。

注意: 退伍军人会记得,重要的是你的索引保持有序 - 如果它们最终不完整或乱序(例如你最终得到 [0][3][1] 由于一些 client-side 删除或排序)然后你会遇到麻烦,因为模型活页夹会按顺序查找。您可以通过添加一些 client-side JS 以在 form-save 上按顺序重新分配名称来解决此问题 - 这很有用。或者您可以继续阅读:

字典呢?

一旦你理解了上面的内容,让字典绑定其实很简单:假设它们真的只是一个 List<KeyValuePair<X,Y>>:

<input name="test[0].Key" value="Item1" />
<input name="test[0].Value" value="Item1Value" />

<input name="test[1].Key" value="Item2" />
<input name="test[1].Value" value="Item2Value" />

但是,如果您提交的索引是 out-of-order,则相同的 issues/limitations 适用。这里的一个特定场景是 checkboxes - 如果你有一个 Dictionary<string, bool> (或任何其他键类型)你想绑定为复选框,你会记得 HTML 不会在表单提交中提交未选中的复选框。因此,如果您只勾选几个项目,您最终会得到一个部分列表,其中的索引肯定是乱序的。

解决方案 1:做 JS 的事 client-side 在表单提交之前重写索引,强制它们是顺序的。完全可行,但很烦人。

解决方案 2:使用 out-of-order 键,帮助模型绑定器了解要查找的键。这很简单:你只需要一个额外的输入,来定义索引:

<input type="hidden" name="test.Index" value="IndexA" />
<input name="test[IndexA].Key" value="IndexAKey" />
<input name="test[IndexA].Value" value="IndexAValue" />

<input type="hidden" name="test.Index" value="SomeOtherIndex" />
<input name="test[SomeOtherIndex].Key" value="SomeOtherIndexKey" />
<input name="test[SomeOtherIndex].Value" value="SomeOtherIndexValue" />

名称为 <collectionName>.Index 的额外输入为模型活页夹提供了整理所有内容所需的推动力。对于字典,使用字典键作为自定义索引很方便,所以一切都排成一行 - 感觉有点多余,但确实有效:

<input type="hidden" name="test.Index" value="IndexAKey" />
<input name="test[IndexAKey].Key" value="IndexAKey" />       //<-- totally feel like this line doesn't need to be there, but it works
<input name="test[IndexAKey].Value" value="IndexAValue" />

为了 full-circle,Dictionary[index].Key 的符号完全是需要发生的 - 但那应该是 string,并且在 HTML 输入 name 属性 - 不在 Html.HiddenFor() 助手中。

大家安息吧!

这是一个有效的 ASP.NET MVC 5 示例,它通过在访问元素之前调用简单但不直观的 ToArray() 方法来克服任何 [*] 问题。这允许您使用数组访问器(以及索引)访问字典元素,而不管您的字典的键类型如何。

我已将此 .cshtml 文件放在我的 Shared/EditorTemplates 文件夹中,因此我现在可以通过调用 @Html.EditorFor(model => model.MyDictionary).[=19= 将它与任何 Dictionary 一起使用]

注意 :不要尝试使用 foreach 循环。 ASP.NET MVC 的模型绑定器确实需要索引,如其他解决方案中所述。

趣味笔记:你实际上可以写一个 foreach 循环,比如 foreach(int i in Enumerable.Range(0, Model.Count))

@model Dictionary<string, bool>

@* 
  BOOTSTRAP V5 only support row-cols-* until * == 6, 
  extend the class if you need to display more items in a row 
*@

<div class="form-group row row-cols-@Model.Count">
  @for (int i = 0; i < Model.Count(); i++)
  {
    <div class="col form-check">
      <div>
        @Html.LabelFor(model => Model.ToArray()[i].Value, Model.ToArray()[i].Key, htmlAttributes: new { @class = "form-check-label" })

        @Html.HiddenFor(model => Model.ToArray()[i].Key)
        @Html.CheckBoxFor(model => Model.ToArray()[i].Value, new { @class = "form-check-input" })
      </div>
    </div>
  }
</div>

编辑 :索引技巧现在在official Blazor documentation(ASP的小弟)中作为'Warning'。 (在链接文档的末尾)