0

I can't seem to figure out how to validate the pieces of a partial view for an ViewModel that has the partial ViewModel as a child object. Here's my lowest level piece, which will ALWAYS be consumed as a partial view inside other form tags:

namespace MVC3App.ViewModels
{
    public class Payment : IValidatableObject
    {
        public decimal Amount { get; set; }
        public int CreditCardNumber { get; set; }

        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            if (Amount < 20)
                yield return new ValidationResult("Please pay more than $20", new string[] { "Amount" });
        }
    }
}

And here's the 'main' ViewModel that includes it:

namespace MVC3App.ViewModels
{
    public class NewCustomerWithPayment :IValidatableObject
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public ViewModels.Payment PaymentInfo { get; set; }

        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            if (Age < 18)
                yield return new ValidationResult("Too young.", new string[] { "Age" });
        }
    }
}

For the View of the NewCustomerWithPayment, I have this:

@model MVC3App.ViewModels.NewCustomerWithPayment
@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>NewCustomerWithPayment</legend>

        <div class="editor-label">
            @Html.LabelFor(model => model.Name)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Name)
            @Html.ValidationMessageFor(model => model.Name)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Age)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Age)
            @Html.ValidationMessageFor(model => model.Age)
        </div>
    </fieldset>
    @Html.Partial("Payment")
    <p><input type="submit" value="Create" /></p>
}

And the Partial View "Payment" is ALWAYS rendered inside another Html.Beginform tag, it just has this:

@model MVC3App.ViewModels.Payment
<h2>Payment</h2>
<fieldset>
    <legend>Payment</legend>

    <div class="editor-label">
        @Html.LabelFor(model => model.Amount)
    </div>
    <div class="editor-field">
        @Html.EditorFor(model => model.Amount)
        @Html.ValidationMessageFor(model => model.Amount)
    </div>

    <div class="editor-label">
        @Html.LabelFor(model => model.CreditCardNumber)
    </div>
    <div class="editor-field">
        @Html.EditorFor(model => model.CreditCardNumber)
        @Html.ValidationMessageFor(model => model.CreditCardNumber)
    </div>
</fieldset>

My problem is that I cannot get the Validation on the 'Payment' viewmodel to work. Can anyone with experience using IValidatableObject on ViewModels which are rendered as Partial Views chime in and give me a validation pattern that works? I can live without JavaScript validation if I have to.

Graham
  • 3,217
  • 1
  • 27
  • 29
  • I don't mean for this to sound sarcastic, but it from the code you've posted, it appears that you are only performing very basic validation on an int (Age) and a decimal (Amount). This is well within the capabilities of data annotations. Why are you writing your own and not using what's built into the framework? – Forty-Two Sep 15 '12 at 03:24
  • The example code is simplified. The actual validation calls will talk to data sources and such. – Graham Sep 17 '12 at 11:56

3 Answers3

1

These answers all have some great info, but my immediate issue was resolved by using this:

@Html.EditorFor(model => model.PaymentInfo)

Instead of this:

Html.Partial("Payment", Model.PaymentInfo)

I was surprised that this worked, but it does. The EditorFor helper renders out the partial view just like Html.Partial, and wires in the validation automatically. For some reason, it does call the validation twice on the child model (Payment in my example), which seems to be a reported issue for some other people (http://mvcextensions.codeplex.com/workitem/10), so I have to include a boolean for 'HasBeenValidated' on each model and check for it at the beginning of the Validate call.

Update: you must move your view to the EditorTemplates folder under /Views/Shared/ in order for the view to be used by the EditorFor helper. Otherwise, the EditorFor will give you the default editing fields for the types.

Graham
  • 3,217
  • 1
  • 27
  • 29
0

Here is a lame example of a custom validator for a checkbox :) I would write a custom validator or use a regex maybe. This may get you on the right path and be easier.

 [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class CheckBoxMustBeTrueAttribute : ValidationAttribute, IClientValidatable
{
    #region IClientValidatable Members

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata,
                                                                           ControllerContext context)
    {
        yield return new ModelClientValidationRule
                         {
                             ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
                             ValidationType = "requiredcheckbox"
                         };
    }

    #endregion

    public override bool IsValid(object value)
    {
        if (value is bool)
        {
            return (bool) value;
        }
        return true;
    }
}
CrazyCoderz
  • 1,351
  • 1
  • 15
  • 30
0

Most probably IValidatableObject is recognized only on the root model. You can call the inner model Validate method from the root model:

public class NewCustomerWithPayment :IValidatableObject {
   ...
   public ViewModels.Payment PaymentInfo { get; set; }

   public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
   {
      if (Age < 18)
         yield return new ValidationResult("Too young.", new string[] { "Age" });

      if (this.PaymentInfo != null)
         yield return this.PaymentInfo.Validate(validationContext);
   }
}

Note: Not sure if the above compiles.

Max Toro
  • 28,282
  • 11
  • 76
  • 114