ViewModel 中集合项的奇怪丢失

Strange loss of collection items in the ViewModel

我的 MVC 代码遇到了一个奇怪的情况。整个上周我已经在互联网上阅读了所有可能的内容,但没有任何线索。今天终于找到了问题,但没有找到解决方案。

场景

我有一个视图必须动态显示不同的下拉列表。当用户 select 从下拉列表中选择一个项目时,将异步调用控制器中的一个操作来检查在视图中显示或隐藏另一个下拉列表的规则。

问题总结

似乎调用模型不涉及的方法在发送回控制器时会影响模型。使用注释的方法,模型被 post 填充回原始数据,当代码处于活动状态时,集合被发送回空的。

代码

给View的模型是:

public class MyViewModel
{
    public string Id { get; set; }
    public List<MyDropDownList> Ddls { get; set; }
    public List<Rule> Rules { get; set; }
}

下拉列表定义如下:

public class MyDropDownList
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string ParentId { get; set; }
    public string SelectedValue { get; set; }
    public IEnumerable<SelectListItem> Items { get; set; }
}

当第一次调用默认 Action 时,View 呈现完美无瑕,这意味着模型中填充了正确的数据,View with Razor 中的小逻辑也起作用了。这里是控制器中的动作:

[HttpGet]
public ActionResult showFirstView()
{
    List<string> selected = GetSelectedIds();
    var model = new MyViewModel('parameters');

    var hiddenIds = new List<string>();
    ApplyRules(GetRulesFromDatabase(), selected, hiddenIds);
    ViewBag.HiddenIds = hiddenIds;

    return View(model);
 }

一些解释:

"GetSelectedIds" 是一个函数,用于遍历 ddls 集合并获取所有 SelectedValues 的 ID(如果有)。 ids的集合稍后交给"ApplyRules"方法。

如果视图的 ID 不在 ViewBag.HiddenIds 集合中,视图将呈现任何 ddl。通过硬编码规则,它工作得很好。

现在,View 呈现一个 PartialView,以便在调用 AJAX 时刷新 DIV 元素,即当 ddl 更改 selection 时。这里是主视图:

/*The main View*/
@model MyViewModel
...
<div class="row">
    @using (Ajax.BeginForm("refreshByAJAX", "MyController", new AjaxOptions { UpdateTargetId = "divToUpdate", InsertionMode = InsertionMode.Replace, HttpMethod = "Get", OnSuccess = "postAjaxOperations()" },this.Model))
    {    
        <div id="divToUpdate">
            @Html.Partial("_myPartialView", Model)
        </div>
     }

</div>
...

(协议应该是 POST 但我只能让它与 GET 一起使用)

这里是局部视图:

/*The partial View*/
@model MyViewModel
...
<div>
    @for (int i = 0; i < Model.Ddls.Count; i++)
    {
       if (!ViewBag.HiddenIds.Contains(Model.Ddls[i].Id))
       { 
           @Html.DropDownListFor(m => m.Ddls[i].SelectedValue, new SelectList(Model.Ddls[i].Items, "Value", "Text", Model.Ddls[i].SelectedValue), new { @class = "some-classes"})
           @Html.HiddenFor(m => m.Ddls[i].Id)
           @Html.HiddenFor(m => m.Ddls[i].Name)
           @Html.HiddenFor(m => m.Ddls[i].SelectedValue)
        }
     }
</div>
...
<input type="submit" value="Ajax call" id="trigger" class="hidden" />

评论:当 ddl 更改 selection 时,我可以 post 成功返回完整模型的唯一方法是通过隐藏输入 "submit",它位于部分的。 ddl "onchange" jquery 触发 "click"。另一个重要的事情是 HiddenFor 助手。没有它们,Binder 无法将模型中的数据持久保存回控制器,但请注意,如果 IF 不为真,则不会创建它们,并且 Ddl 无论如何都会从集合中丢失。

第一个问题,已解决

当在 ddl 中 select 编辑一个项目时,将异步调用控制器中的操作 (Ajax.BeginForm) 以刷新下拉列表。但它发送的模型完全为空(或模型中的某些集合为空)(空)。这非常棘手,但我收到 Ajax 调用以将整个模型正确发送回 Action。

为了简单起见,show/hide 视图中其他下拉列表的规则是硬编码的,并且视图会正确刷新正确的 ddl。这里调用的 Action 是 Ajax:

/* Method called by AJAX */
public PartialViewResult refreshByAJAX(MyViewModel model)
{
    List<string> selected = GetSelectedIds(model);
    model = new MyViewModel('params');

    var hiddenIds = new List<string>();
    ApplyRules(GetRulesFromDatabase(), selected, hiddenIds);
    ViewBag.HiddenIds = hiddenIds;

    return PartialView("_myPartialView", model);
}

当前问题

现在,我将硬编码逻辑替换为真实逻辑,从数据库中获取规则。有了这个视图正确填充了正确的下拉列表,但是当我 select 一个项目时,对 Action 的 Ajax 调用再次发送带有所有 Ddls 集合 EMPTY 的模型!

您可能很快就会说 hide/show 下拉菜单的逻辑显然是错误的。可能是这样,因为如果我评论它,问题就解决了。但是它有什么作用呢?只是将字符串 Id 添加到 ViewBag 中的集合。这里是决定性的 'ApplyRules' 方法:

/* Method in the controller (not Action)*/
private void ApplyRules(List<Rule> rules, List<string> selected, List<string> hiddenIds)
{
    foreach (var rule in rules)
    {
        if (rule.Action == "show")
        {
            if (!selected.Contains(rule.RequiredDdlId))
            {
                 hiddenIds.Add(rule.AffectedDdlId);
            }
         }
     }
}

规则class,虽然不相关:

public class Rule 
{
    public string Action { get; set; }  // "Hide" or "Show"
    public string AffectedDdlId { get; set; }
    public string RequiredDdlId { get; set; }
}

您可以看到 'ApplyRules' 方法没有接触模型。但是如果我评论它,那么模型将与集合中的所有 Ddl 一起返回给控制器。如果它处于活动状态,则 Ddls 集合 posted 为空(null)。

测试用例有2个ddls。主 ddl 始终可见。在默认操作中,第二个 ddl 默认与 "ApplyRules" 的第一个 运行 一起隐藏,并且只有当特定项目在主 ddl 中被 selected 时才必须显示。然后调用 AJAX 方法,"ApplyRules" 方法应该将其从 ViewBag.HiddenIds 中删除,但是之前失败,因为集合为空。如果注释了 "ApplyRules" 方法,则只有主 ddl 在集合中被 post 回传,这是正确的(因为如果 IF 为假,则不会生成 HiddenFor 帮助程序,而 Binder 会丢失它) .但是如果 "ApplyRules" 方法被激活,两者都会从集合中移除。 评论:主下拉列表始终可见。没有设置隐藏的规则,即使在 postback 之后也是可见的。到处都是断点,我调试并检查了所有值是否符合所有时间线的预期。但是,如果 "ApplyRules" 处于活动状态,即使这个主要的 Ddl 也会从集合中删除。

此外,第一种方法是使用 "myDropDownList" class 本身中的字段将其设置为 "visible" 或 "hidden"。归咎于此,我改为使用 ViewBag 而不是触摸模型,结果同样失败。

预先感谢您提供任何理论。如果有帮助或回答问题,我很乐意添加额外的信息。

没错! Stephen Muecke 是完全正确的。问题解决了。

实际上,Ddls 集合从索引 1 开始序列化。("hide" 规则影响集合中的第一个元素)。以下是为 HiddenFor 属性生成的 ID:

 <input id="Ddls_1__Id" name="Ddls[1].Id" value=... type="hidden">
 <input id="Ddls_1__Name" name="Ddls[1].Name" value="..." type="hidden">

所以我让它从零索引开始,集合再次绑定并在 AJAX 调用中提供给控制器。

为了测试它,我更改了 PartialView 中的 show/hide 逻辑。相反,仅当规则为 "show" 时才创建下拉列表(及其 HiddenFor 助手),我将其更改为始终创建所有下拉列表,但在 htmlAttributes 中附加 class "hidden" 使它们不可见.

/*The adjusted partial View*/
 @model MyViewModel
 ...
 <div>
     @for (int i = 0; i < Model.Ddls.Count; i++)
     {
        string hiddenClass = "";
        // The string hiddenClass will be "hidden" only for the Ddls marked as hidden from the ApplyRules method.
        if (ViewBag.HiddenIds.Contains(Model.Categories[i].Id))
        {
            hiddenClass = "hidden";   
        }
        // The objects are created even if are hidden
        @Html.DropDownListFor(m => m.Ddls[i].SelectedValue, new SelectList(Model.Ddls[i].Items, "Value", "Text", Model.Ddls[i].SelectedValue), new { @class = "some-classes " + hiddenClass})
        @Html.HiddenFor(m => m.Ddls[i].Id)
        @Html.HiddenFor(m => m.Ddls[i].Name)
      }
 </div>
 ...
 <input type="submit" value="Ajax call" id="trigger" class="hidden" />

感谢斯蒂芬的大力帮助。