Проверка ASP.NET MVC 3 на вложенных объектах не работает должным образом - дважды проверяет дочерний объект, а не родительский объект - PullRequest
7 голосов
/ 20 марта 2012

Я пытаюсь заставить 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» получает двойное попадание, но метод «Проверка» в «OuterObject» вообще не выполняется.

Если вы удалите IValidatableObject из «InnerObject», то попадет в «OuterObject».

Это ошибка, или я должен ожидать, что она будет работать таким образом? Если я ожидаю, что лучший обходной путь?

Ответы [ 3 ]

1 голос
/ 21 марта 2012

Этот ответ только для того, чтобы предложить один из обходных путей, о котором я только что подумал - так что на самом деле это не ответ!Я до сих пор не уверен, является ли это ошибкой или каков лучший обходной путь, но вот один из вариантов.

Если вы удалите пользовательскую логику проверки из «InnerObject» и включите ее в «OuterObject», она кажетсяотлично работаетТаким образом, в основном это работает вокруг ошибки, позволяя только самому верхнему объекту иметь любую пользовательскую проверку.

Вот новый 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 голос
/ 02 февраля 2013

Я не уверен, что это проблема с 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 голосов
/ 20 марта 2012

Должны ли вы создать базовый класс OuterObject для InnerObject вместо создания отношений, как вы это сделали? (Или наоборот) и предоставить представление базового объекта как ViewModel?

Это будет означать, что при привязке модели конструктор по умолчанию OuterObject (или какой класс всегда является вашей базой) будет вызываться косвенно, вызывая Validate для обоих объектов.

т.е. Класс:

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

Вид:

@model MvcNestedObjectTest.Models.OuterObject

Действие контроллера:

public ActionResult Index(OuterObject model)
...