Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVC Client-Side Validation for EditorFor in foreach

So I have a view with the following structure (this isn't the actual code, but a summary):

@using (Html.BeginForm("Action", "Controller", FormMethod.Post))
{
  @Html.ValidationSummary("", new { @class = "text-danger" })
  <table>
    <thead>
      <tr>
        <th>Column1</th>
        <th>Column2</th>
      </tr>
    </thead>
    <tbody id="myTableBody">
      @for (int i = 0; i < Model.Components.Count; i++)
      {
        @Html.EditorFor(m => m.MyCollection[i])
      }
    </tbody>
    <tfoot>
      <tr>
        <td>
          <button id="btnAddRow" type="button">MyButton</button>
        </td>
      </tr>
    </tfoot>
  </table>

  <input type="button" id="btnSubmit" />
}

@section scripts {
  @Scripts.Render("~/Scripts/MyJs.js")
}

The EditorFor is rendering markup that represents rows bound to properties in MyCollection. Here's a sample snippet of how the editor template looks:

@model MyProject.Models.MyCollectionClass

<tr>
  <td>
    @Html.TextBoxFor(m => m.Name)
  </td>
  <td>
    @Html.DropDownListFor(m => m.Type, Model.AvailableTypes)
  </td>
</tr>

Basically, my problem is that the client-side validation won't fire for the elements inside of the editor template as it should. Could someone point me in the right direction of where I might be going wrong with this.

Also, please note that the following is set in my web.config.

<appSettings>
  <add key="ClientValidationEnabled" value="true" />
  <add key="UnobtrusiveJavaScriptEnabled" value="true" />
</appSettings>

and also MyCollectionClass has proper [Require] annotations on the properties that shall be enforced. Another thing to note is that checking

if(ModelState.IsValid)
{
}

Returns false, as expected, if the required fields aren't right. The problem there is that I want client-side validation and not server-side. One of my other pages is implementing jQuery validation, but does not contain all the nesting that this scenario does and thus it works correctly.

Thanks in advance.

like image 795
Brandon Avant Avatar asked Oct 31 '22 23:10

Brandon Avant


1 Answers

From what I Have learnt MVC Doesn't really provide 'out of the box' client side validation. there are third party options but I prefer to do my own thing so I handballed the whole thing in JavaScript. To compound matters with the EditorFor doesn't allow adding html attributes like other Helper Methods.

My Fix for this was fairly complex but I felt comprehensive, I hope you find it as helpful as I have

First Overload the HtmlHelper EditorFor in .Net

public static HtmlString EditBlockFor<T, TValue>(this HtmlHelper<T> helper, Expression<System.Func<T, TValue>> prop, bool required)
    {   
        string Block = "";
        Block += "<div class='UKWctl UKWEditBox' " +
            "data-oldvalue='" + helper.ValueFor(prop) + "' " +
            "data-required='" + required.ToString() + ">";
        Block += helper.EditorFor(prop);
        Block += "</div>";

        return new HtmlString(Block);
    }

add the new editBlockfor in razor view (just as you are) but alter the Begin Form method to add a Name and id to the form element so you can identify it later

    @using (Html.BeginForm("Action", "Controller", FormMethod.Post, new { name = "MyDataForm", id = "MyDataForm" }))

Then when the User Clicks Save, Run the validation method from JavaScript

function validate(container)    {

    var valid = true;
    //use jquery to iterate your overloaded editors controls fist and clear any old validation
    $(container).find(".UKWctl").each(function (index) { clearValidation($(this)); });
    //then itterate Specific validation requirements 
    $(container).find(".UKWctl[data-required='True']").each(function (index) {
        var editobj = getUKWEdit(this);
        if (editobj.val() == "") {
            valid = false;
            //use this Method to actually add the errors to the element
            AddValidationError(editobj, 'This field, is required');
            //now add the Handlers to the element to show or hide  the valdation popup
            $(editobj).on('mouseenter', function (evt) { showvalidationContext(editobj, evt); });
            $(editobj).on('mouseout', function () { hidevalidationContext(); });
            //finally add a new class to the element so that extra styling can be added to indicate an issue 
        $(editobj).addClass('valerror');
    }
    });
    //return the result so the methods can be used as a bool
    return valid;
}

Add Validation Method

function AddValidationError(element, error) {
    //first check to see if we have a validation attribute using jQuery
    var errorList = $(element).attr('data-validationerror');
    //If not Create a new Array() 
    if (!errorList || errorList.length < 1) {
        errorList = new Array();
    } else {
        //if we have, parse the Data from Json
        var tmpobj = jQuery.parseJSON(errorList);
        //use jquery.Map to convert it to an Array()
       errorList = $.map(tmpobj, function (el) { return el; });
    }
   if ($.inArray(error, errorList) < 0) {
        // no point in add the same Error twice (just in case)
        errorList.push(error);
    }
    //then stringyfy the data backl to JSON and add it to a Data attribute     on your element using jQuery
     $(element).attr('data-validationerror', JSON.stringify(errorList));
}

Lastly Show and hide the actual Errors, In order to facilitate this easily I slipped in a little div element to the _Layout.html

    <div id="ValidataionErrors" title="" style="display:none">
        <h3 class="error">Validation Error</h3>
        <p>This item contatins a validation Error and Preventing Saving</p>
        <p class="validationp"></p>
    </div>

Show

var tipdelay;
function showvalidationContext(sender, evt)
{
    //return if for what ever reason the validationError is missing
    if ($(sender).attr('data-validationerror') == "") return;
    //Parse the Error to an Object 
    var jsonErrors = jQuery.parseJSON($(sender).attr('data-validationerror'));
    var errorString = '';
//itterate the Errors from the List and build an 'ErrorString'
    for (var i = 0; i <= jsonErrors.length; i++)
    {
        if (jsonErrors[i]) {
            //if we already have some data slip in a line break
            if (errorString.length > 0) { errorString += '<br>'; }
            errorString += jsonErrors[i];
        }
    }
//we don't want to trigger the tip immediatly so delay it for just a moment 
    tipdelay = setTimeout(function () {
        //find the p tag tip if the tip element  
        var validationError = $('#ValidataionErrors').find('.validationp');
        //then set the html to the ErrorString 
        $(validationError).html(errorString);
        //finally actually show the tip using jQuery, you can use the     evt to find the mouse position 
        $('#ValidataionErrors').css('top', evt.clientY);
        $('#ValidataionErrors').css('left', evt.clientX);
        //make sure that the tip appears over everything 
        $('#ValidataionErrors').css('z-index', '1000');
        $('#ValidataionErrors').show();
    }, 500);
}    

Hide (Its much easier to hide)

function hidevalidationContext() {
    //clear out the tipdelay
    clearTimeout(tipdelay);
    //use jquery to hide the popup
    $('#ValidataionErrors').css('top', '-1000000px');
    $('#ValidataionErrors').css('left', '-1000000px');
    $('#ValidataionErrors').css('z-index', '-1000');
    $('#ValidataionErrors').hide();
}

for usage you can try some thing like

function save()
{
    if (validate($("#MyDataForm")))
    {
         $("#MyDataForm").submit();   
    }
    else {
        //all the leg has been done this stage so perhaps do nothing
    }
}

here is My Css for the validation Popup

#ValidataionErrors {
    display: block; 
    height: auto;
    width: 300px;
    background-color: white;
    border: 1px solid black;
    position: absolute;
    text-align: center;
    font-size: 10px;
}

#ValidataionErrors h3 { border: 2px solid red; }
.valerror { box-shadow: 0 0 2px 1px red; }
like image 57
Ian Avatar answered Nov 11 '22 05:11

Ian