2

Since I'm handling a extremely complex model and forms I will reduce my problem in a more understandable example (please excuse if there is any typo).

First I will show the scenario:

The model...

public class Foo
{
    [Required]
    public int fooId {get; set;}
    public string fooName {get; set;}

    public List<Bar> barList {get; set;}
}


public class Bar
{
    [Required]
    public int barId {get; set;}
    public string barName {get; set;}
}

The view...

@model Foo

@using (Html.BeginForm("Save", "form", FormMethod.Post))
{

    <div class="control-group">  
        <div class="controls">  
            @Html.TextBoxFor(model => Model.fooId)
       </div>
    </div> 

    <div class="control-group">  
        <div class="controls">  
            @Html.TextBoxFor(model => Model.fooName)
        </div>
    </div> 

    @for (int i = 0; i < Model.barList.Count(); i++)
    { 
        @Html.EditorFor(m => m.barList[i])
    }

}

The "bar" editor template...

@model Bar

<div class="control-group">  
    <div class="controls">  
        @Html.TextBoxFor(m => m.barId)
    </div>
</div> 
<div class="control-group">  
    <div class="controls">  
        @Html.TextBoxFor(m => m.barName)
    </div>
</div> 

The problem that I'm having is during the client-side validation for inputs in nested collections, in this case I'm not able to validate the "barId" input field. It simply ignores it... In the case of the fooId field, it's validated OK.

If we go deeper, a "foo" object with 2 "bar" items would generate something like this:

<div class="control-group">  
    <div class="controls">  
        <input class="input-validation-error" id="fooId" name="fooId" type="text" value=""> 
    </div>
</div>
<div class="control-group">  
    <div class="controls">  
        <input id="fooName" name="fooName" type="text" value=""> 
    </div>
</div>

<div class="control-group">  
    <div class="controls">  
        <input id="barList_0__barId" name="barList[0].barId" type="text" value=""> 
    </div>
</div>
<div class="control-group">  
    <div class="controls">  
        <input id="barList_0__barName" name="barList[0].barName" type="text" value=""> 
    </div>
</div>

<div class="control-group">  
    <div class="controls">  
        <input id="barList_1__barId" name="barList[1].barId" type="text" value=""> 
    </div>
</div>
<div class="control-group">  
    <div class="controls">  
        <input id="barList_1__barName" name="barList[1].barName" type="text" value=""> 
    </div>
</div>

As you can see, the items inside the "bar" collection have received a different id and name. This is the normal behaviour for rendering the collections.

But it seems to be that the client-side validation doesn't work with these ids and names. The validation will work only if I change the id & name to "barId", removing the collection index..

After hours of investigation, I've found some articles and posts regarding issues like this, but nothing concrete and I still could not solve this.

IValidatableObject in MVC3 - client side validation

mvc clientside validation for nested (collection) properties

Community
  • 1
  • 1
Javier
  • 2,093
  • 35
  • 50
  • The output you posted doesn't seem to match your view and editor template, shouldn't the top two be Foo controls and each Bar have Id and Name controls? – christofr Jan 18 '14 at 22:33
  • Yes, sorry about that. Now it's corrected – Javier Jan 19 '14 at 14:20
  • If you're adding your collection items using javascript, are you refreshing your validation? For example, `var form = $("#editItem");` `form.removeData('validator');` `form.removeData('unobtrusiveValidation');` `$.validator.unobtrusive.parse(form);` is what I call after adding my add method for a collection item. – David T. Macknet Apr 29 '14 at 20:39
  • 1
    Did you get any answer to your problem? – gunnerz Sep 26 '14 at 14:36

1 Answers1

0

I did not find a solution, but I did find a workaround.

Functional explanation: the Model is called "InsuranceLine", and it has a collection "InsuranceLine.Letters." Each Letter has a nullable Boolean property "Letter.IsDeficient". If "IsDeficient" is changed from False to True then the string field "Letter.ReasonCode" is required. "IsDeficient" is rendered as a checkbox, and "ReasonCode" is rendered as two radio buttons, "Corrected" and "Waived".

Here is the custom attribute:

    public class ReasonCodeAttribute : ValidationAttribute, IClientValidatable
    {
        private const string errorMessage = "When 'Deficient' is changed from True to False you must select a Reason.";

        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            yield return new ModelClientValidationRule
            {
                ErrorMessage = errorMessage,
                ValidationType = "reasoncoderequired"
            };
        }

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            Letter letter = validationContext.ObjectInstance as Letter;
            if(!letter.IsDeficient.GetValueOrDefault() 
                && letter.IsDeficient_OriginalState.GetValueOrDefault()
                && (value == null || string.IsNullOrEmpty(value.ToString())))
            {
                return new ValidationResult(errorMessage);
            }
            else
            {
                return null;
            }
        }
    }

I decorate Letter.ReasonCode with the custom attribute:

[ReasonCodeAttribute]
public string ReasonCode { get; set; }

I render the nested Letters collection in the *.cshtml page:

@for (int i = 0; i < Model.Letters.Count; i++)
{
@Html.EditorFor(m => m.Letters[i].IsDeficient, "MyCustomTemplate", new { htmlAttributes  = new { @class="cb-is-deficient" } })
<div class="rb-reason-code">
    <label>@Html.RadioButtonFor(m => m.Letters[i].ReasonCode, myVar == "C", new { id = id + "C", @class ="rb-reason-code" }) Corrected</label>
    &nbsp;&nbsp;
    <label>@Html.RadioButtonFor(m => m.Letters[i].ReasonCode, myVar == "W", new { id = id + "W", @class = "rb-reason-code" }) Waived</label>
</div>
}

The GetClientValidationRules() method of the ReasonCode attribute causes the asp.net runtime to generate the following attribute when it renders the ReasonCode into an html radio button:

data-val-reasoncoderequired="When 'Deficient' is changed from True to False you must select a Reason.".

In JavaScript I add the 'reasoncoderequired' method to the validator like so in the document ready method. As part of my workaround I need to manually add the class "error" to my display so that the user gets a visual hint of the invalid state of the model:

$.validator.addMethod('reasoncoderequired', function (value, element) {
        var $parent = $(element).closest('div.parent');
        var $cb = $parent.find('input[type="checkbox"].cb-is-deficient');
        if ($cb.prop('defaultChecked')) {
            var $selectedRadioButton = $parent.find('div.rb-reason-code').find('input[type="radio"]:checked');
            if ($selectedRadioButton.length == 0) {
                $parent.addClass('error');
                return false;
            }
        }
        $parent.removeClass('error');
        return true;
    });

Finally, I add the reasoncoderequired rule to each ReasonCode radio button like so, also in the document ready method. The "messages" simply reads from the data-val-reasoncoderequired attribute rendered with each input to display the error message:

        $form.find('input[type="radio"].rb-reason-code').each(function () {
        $(this).rules('add',
            {
                reasoncoderequired: true,
                messages: { reasoncoderequired: $(this).attr('data-val-reasoncoderequired') }
            });
    })
Tom Regan
  • 3,580
  • 4
  • 42
  • 71