ASP.NET MVC 3中嵌套对象的验证未能按预期工作 - 验证子对象两次而不是父对象。

8
我将尝试让ASP.NET MVC 3从复杂的嵌套对象中生成表单。我发现了一种意外的验证行为,我不确定它是否是DefaultModelBinder中的错误。
如果我有两个对象,我们称之为“父”对象“OuterObject”,并且它有一个类型为“InnerObject”的属性(子对象):
    public class OuterObject : IValidatableObject
{
    [Required]
    public string OuterObjectName { get; set; }

    public InnerObject FirstInnerObject { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (!string.IsNullOrWhiteSpace(OuterObjectName) && string.Equals(OuterObjectName, "test", StringComparison.CurrentCultureIgnoreCase))
        {
            yield return new ValidationResult("OuterObjectName must not be 'test'", new[] { "OuterObjectName" });
        }
    }
}

这里是InnerObject:

    public class InnerObject : IValidatableObject
{
    [Required]
    public string InnerObjectName { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (!string.IsNullOrWhiteSpace(InnerObjectName) && string.Equals(InnerObjectName, "test", StringComparison.CurrentCultureIgnoreCase))
        {
            yield return new ValidationResult("InnerObjectName must not be 'test'", new[] { "InnerObjectName" });
        }
    }
}

您会注意到我对两个值都进行了验证,这只是一些虚拟的验证,用于说明某些值不能等于“test”。

这是将显示在其中的视图(Index.cshtml):

@model MvcNestedObjectTest.Models.OuterObject
@{
    ViewBag.Title = "Home Page";
}

@using (Html.BeginForm()) {
<div>
    <fieldset>
        <legend>Using "For" Lambda</legend>

        <div class="editor-label">
            @Html.LabelFor(m => m.OuterObjectName)
        </div>
        <div class="editor-field">
            @Html.TextBoxFor(m => m.OuterObjectName)
            @Html.ValidationMessageFor(m => m.OuterObjectName)
        </div>

        <div class="editor-label">
            @Html.LabelFor(m => m.FirstInnerObject.InnerObjectName)
        </div>
        <div class="editor-field">
            @Html.TextBoxFor(m => m.FirstInnerObject.InnerObjectName)
            @Html.ValidationMessageFor(m => m.FirstInnerObject.InnerObjectName)
        </div>

        <p>
            <input type="submit" value="Test Submit" />
        </p>
    </fieldset>
</div>
}

最后是HomeController:

    public class HomeController : Controller
{
    public ActionResult Index()
    {
        var model = new OuterObject();
        model.FirstInnerObject = new InnerObject();
        return View(model);
    }

    [HttpPost]
    public ActionResult Index(OuterObject model)
    {
        if (ModelState.IsValid)
        {
            return RedirectToAction("Index");
        }
        return View(model);
    }
}

您会发现,当模型被DefaultModelBinder验证时,“InnerObject”中的“Validate”方法会被调用两次,但“OuterObject”中的“Validate”方法根本不会被调用。
如果您从“InnerObject”中删除IValidatableObject,则“OuterObject”上的IValidatableObject将被调用。
这是一个bug吗?还是我应该期望它以这种方式工作?如果我应该期望它,有什么最好的解决方法?
3个回答

1

这个答案仅提供了我刚想到的一种解决方法,所以它实际上并不是一个答案!我仍然不确定这是一个 bug 还是最佳的解决方法,但这里有一种选项。

如果你从 "InnerObject" 中删除自定义验证逻辑,并将其合并到 "OuterObject" 中,那么它似乎可以正常工作。因此,基本上通过只允许最顶层的对象具有任何自定义验证来解决了该 bug。

这是新的 InnerObject:

    //NOTE: have taken IValidatableObject off as this causes the issue - we must remember to validate it manually in the "Parent"!
public class InnerObject //: IValidatableObject
{
    [Required]
    public string InnerObjectName { get; set; }
}

这里是新的OuterObject(其中包含从InnerObject中窃取的验证代码):

    public class OuterObject : IValidatableObject
{
    [Required]
    public string OuterObjectName { get; set; }

    public InnerObject FirstInnerObject { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (!string.IsNullOrWhiteSpace(OuterObjectName) && string.Equals(OuterObjectName, "test", StringComparison.CurrentCultureIgnoreCase))
        {
            yield return new ValidationResult("OuterObjectName must not be 'test'", new[] { "OuterObjectName" });
        }

        if (FirstInnerObject != null)
        {
            if (!string.IsNullOrWhiteSpace(FirstInnerObject.InnerObjectName) &&
                string.Equals(FirstInnerObject.InnerObjectName, "test", StringComparison.CurrentCultureIgnoreCase))
            {
                yield return new ValidationResult("InnerObjectName must not be 'test'", new[] { "FirstInnerObject.InnerObjectName" });
            }
        }
    }
}

这个方案的效果符合我的预期,能够正确地将验证错误与每个字段连接起来。

但这并不是一个很好的解决方案,因为如果我需要将“InnerObject”嵌套在其他类中,它就无法共享该验证 - 我需要复制它。显然,我可以在类上拥有一个方法来存储逻辑,但每个“父”类都需要记得“验证”子类。


1
我不确定这是否还是MVC 4的问题,但是如果您使用专门为InnerObjects制作的局部视图,它们将会正确验证。
<fieldset>
    <legend>Using "For" Lambda</legend>

    <div class="editor-label">
        @Html.LabelFor(m => m.OuterObjectName)
    </div>
    <div class="editor-field">
        @Html.TextBoxFor(m => m.OuterObjectName)
        @Html.ValidationMessageFor(m => m.OuterObjectName)
    </div>

    @Html.Partial("_InnerObject", Model.InnerObject)

    <p>
        <input type="submit" value="Test Submit" />
    </p>
</fieldset>

然后添加这个部分 "_InnerObject.cshtml":
@model InnerObject

    <div class="editor-label">
        @Html.LabelFor(m => m.InnerObjectName)
    </div>
    <div class="editor-field">
        @Html.TextBoxFor(m => m.InnerObjectName)
        @Html.ValidationMessageFor(m => m.InnerObjectName)
    </div>

0

你是否应该将OuterObject作为InnerObject的基类,而不是像你现在做的那样创建一个关系?(或者反过来),并将基础对象视为ViewModel呢?

这意味着,在模型绑定时,将间接调用OuterObject(或任何其他基类)的默认构造函数,从而验证两个对象。

i.e. Class:

public class OuterObject : InnerObject, IValidateableObject
{
...
}

视图:

@model MvcNestedObjectTest.Models.OuterObject

控制器行动:

public ActionResult Index(OuterObject model)

谢谢,我已经考虑过了,对于这种特定情况它可以工作,但是随着对象变得更加复杂,它将无法使用。例如,如果我需要InnerObject1、SomeString、InnerObject2、SomeOtherString(即其他属性之间的嵌套对象)。 - nootn
@nootn 你尝试过使用Fluent Validation吗?它是一种高级的验证依赖项和嵌套验证规则的方式。 - amythn04
我已经研究过这个,看起来不错,但在我们的组织中,我们选择使用内置的数据注释作为MVC中验证模型的标准。我想我们可以使用组合,但仍然无法避免这个事实,它的行为不符合预期,有点陷阱。 - nootn

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