Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Remote validation with AdditionalFields on collection of Models

Remote validation works fine when I've got just one instance of my model on the view.

Problem is when my view is dealing with collection of models. Here is my model :

public class TableFormTestModel
{
    public GridRow[] GridData { get; set; }
    public class GridRow
    {
        public Int32 Id { get; set; }

        [Required, StringLength(50), Remote("IsNameAvailable", "TableFormTest", "Admin", AdditionalFields = "Id")]
        public String Name { get; set; }
    }
}

In my view I've got :

@model TableFormTestModel
@using (Html.BeginForm())
{
    Html.EnableClientValidation();
    Html.EnableUnobtrusiveJavaScript();
    for(var i = 0;i<Model.GridData.Length;i++)
    {
    <div>
        @Html.HiddenFor(x => Model.GridData[i].Id)
        @Html.TextBoxFor(x => Model.GridData[i].Name)
        @Html.ValidationMessageFor(x => Model.GridData[i].Name)    
    </div>
    }
}

This is quite a long way of generating the form, can anyone improve the syntax for me please?

Following html form is produced :

<form method="post" action="/Admin/TableFormTest/">    <div>
    <input type="hidden" value="1" name="GridData[0].Id" id="GridData_0__Id" data-val-required="The Id field is required." data-val-number="The field Id must be a number." data-val="true">
    <input type="text" value="abc" name="GridData[0].Name" id="GridData_0__Name" data-val-required="The Name field is required." data-val-remote-url="/Admin/TableFormTest/IsNameAvailable" data-val-remote-additionalfields="*.Name,*.Id" data-val-remote="&amp;#39;Name&amp;#39; is invalid." data-val-length-max="50" data-val-length="The field Name must be a string with a maximum length of 50." data-val="true">
    <span data-valmsg-replace="true" data-valmsg-for="GridData[0].Name" class="field-validation-valid"></span>    
</div>
<div>
    <input type="hidden" value="2" name="GridData[1].Id" id="GridData_1__Id" data-val-required="The Id field is required." data-val-number="The field Id must be a number." data-val="true">
    <input type="text" value="def" name="GridData[1].Name" id="GridData_1__Name" data-val-required="The Name field is required." data-val-remote-url="/Admin/TableFormTest/IsNameAvailable" data-val-remote-additionalfields="*.Name,*.Id" data-val-remote="&amp;#39;Name&amp;#39; is invalid." data-val-length-max="50" data-val-length="The field Name must be a string with a maximum length of 50." data-val="true">
    <span data-valmsg-replace="true" data-valmsg-for="GridData[1].Name" class="field-validation-valid"></span>    
</div>

Although above html looks fairly well ( each Model from the collection has got unique id and name ) there is a problem with additional fields on remote validation :

data-val-remote-additionalfields="*.Name,*.Id"

Id from the first row gets picked up when remote validation is fired on the second row.

like image 535
mb666 Avatar asked Nov 15 '22 01:11

mb666


1 Answers

Firstly, yes, you can improve the syntax of your view. Use EditorTemplates.

Create Views\Shared\EditorTemplates\GridRow.cshtml:

@model TestMvc.Models.TableFormTestModel.GridRow
<div>
    @Html.HiddenFor(x => x.Id)
    @Html.TextBoxFor(x => x.Name)
    @Html.ValidationMessageFor(x => x.Name)
</div>

Now your main view only needs to be:

@model TableFormTestModel
@using (Html.BeginForm())
{
    Html.EnableClientValidation();
    Html.EnableUnobtrusiveJavaScript();
    @Html.EditorFor(x => x.GridData)
}

As for RemoteAttribute troubles, it's tricky. The problem is due to the names of the inputs that MVC creates for arrays. As you can see, your inputs are named, eg, GridData[1].Id, GridData[1].Name (etc). Well, jQuery makes its ajax call by supplying those names to the querystring.

Thus, what ends up getting called is

/Admin/TableFormTest/IsNameAvailable?GridData%5B1%5D.Name=sdf&GridData%5B1%5D.Id=5 

aka

/Admin/TableFormTest/IsNameAvailable?GridData[1].Name=sdf&GridData[1].Id=5 

...and the default model binder really doesn't know what to do with that.

What I suggest is to write your own custom model binder. Tell MVC how to read this query string and then make the object you want.

Here's a proof-of-concept. (But do not use this thing in production: it takes too many assumptions and will crash and burn on anything unexpected.)

public class JsonGridRowModelBinder : IModelBinder {

    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
        var model = new TableFormTestModel.GridRow();
        var queryString = controllerContext.HttpContext.Request.QueryString;
        model.Name = queryString[queryString.AllKeys.Single(x => x.EndsWith("Name"))];
        string id = queryString[queryString.AllKeys.Single(x => x.EndsWith("Id"))];
        model.Id = string.IsNullOrWhiteSpace(id) ? 0 : int.Parse(id);

        return model;
    }

}

Then, tell your IsNameAvailable method to use this model binder:

public JsonResult IsNameAvailable([ModelBinder(typeof(JsonGridRowModelBinder))] TableFormTestModel.GridRow gridRow) {
    ...
}
like image 66
Ber'Zophus Avatar answered May 01 '23 13:05

Ber'Zophus