ASP.NET MVC 3 Checking for nested objects does not work as expected - it checks the child twice, not the parent

I am trying to get ASP.NET MVC 3 to create forms from complex, nested objects. I found one validation behavior that was unexpected, and I'm not sure if this is an error in DefaultModelBinder or not.

If I have two objects, the parent is called OuterObject and has a property of type InnerObject (child):

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" }); } } } 

Here's 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" }); } } } 

You will notice that I put both on them. Only some dummy checks to say that a value cannot be equal to a โ€œtestโ€.

Here is the view to be displayed in (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> } 

.. and finally this is the 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); } } 

What you will find is that when the model receives the default confirmation of ModelBinder, the Validate method in InnerObject gets twice, but the Validate method does not get in OuterObject at all.

If you delete the IValidatableObject file from "InnerObject", one of the "OuterObject" will be deleted.

Is this a mistake, or should I expect it to work that way? Should I expect this to be the best workaround?

+7
source share
3 answers

This answer is only to provide one workaround that I just thought about - so this is actually not the answer! I'm still not sure if this is a mistake or the best workaround, but here is one option.

If you remove the custom validation logic from "InnerObject" and include it in "OuterObject", it works fine. Thus, basically it works around the error, allowing only the very first object to have any special check.

Here is the new 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; } } 

And here is the new OuterObject (with validation code stolen from 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" }); } } } } 

This works as I expected by correctly connecting the validation error to each field.

This is not a great solution, because if I need to nest "InnerObject" in some other class, it does not share this check - I need to replicate it. Obviously, I could have a method in the class for storing logic, but each โ€œparentโ€ class should remember the โ€œCheckโ€ child class.

+1
source

I'm not sure if this is a problem with MVC 4, but ...

If you use partial views made only for your InnerObjects, they will be validated correctly.

 <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> 

Then add this partial "_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> 
+1
source

Should you make an OuterObject base class for InnerObject instead of creating relationships like you do? (Or vice versa) and provide a representation of the underlying object as a ViewModel?

This will mean that when you bind the model, the default constructor for OuterObject (or which is your base ever) will be called by an indirect Validate call on both objects.

i.e. Grade:

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

View:

 @model MvcNestedObjectTest.Models.OuterObject 

Controller action:

 public ActionResult Index(OuterObject model) 
0
source

All Articles