jquery.validate lost on ajax replacement and only

2019-08-09 09:14发布

问题:

I am using jquery.validate in MVC 2 with MicrosoftMvcJQueryValidation. I have data annotations on my model which is then being translated into jquery validators. I am using a modification to MicrosoftMvcJQueryValidation as outlined by Soe Tun to allow my error messages to appear in a validation summary instead of beside the controls.

When the page loads, everything works as expected. The problem is that I am using ajax forms with replace mode to rewrite the form. When I do this, I lose all of my client side validation.

Validation still happens server side, and the fields that have errors are correctly being given the css classes to change their style. However, only the last error message is being shown in my validation summary.

The controller isn't anything special. If the model is valid, do work, otherwise return the same model back into the view.

Here's a sample of my ajax form

<% using (Ajax.BeginForm("AddCreditCard", "Dashboard",
       new { },
       new AjaxOptions() { 
           HttpMethod = "Post",
           InsertionMode = InsertionMode.Replace,
           UpdateTargetId = "quickpay-wrapper",
           OnSuccess = "newPaymentSetup",
           LoadingElementId = "loading-pane"
            }, new { id="new-credit-card-form" })) { %>

Here is the modified javascript.

jQuery.validator.addMethod("regex", function(value, element, params) {
    if (this.optional(element)) {
        return true;
    }

    var match = new RegExp(params).exec(value);
    return (match && (match.index == 0) && (match[0].length == value.length));
});

// glue

function __MVC_ApplyValidator_Range(object, min, max) {
    object["range"] = [min, max];
}

function __MVC_ApplyValidator_RegularExpression(object, pattern) {
    object["regex"] = pattern;
}

function __MVC_ApplyValidator_Required(object) {
    object["required"] = true;
}

function __MVC_ApplyValidator_StringLength(object, maxLength) {
    object["maxlength"] = maxLength;
}

function __MVC_ApplyValidator_Unknown(object, validationType, validationParameters) {
    object[validationType] = validationParameters;
}

function __MVC_CreateFieldToValidationMessageMapping(validationFields) {
    var mapping = {};

    for (var i = 0; i < validationFields.length; i++) {
        var thisField = validationFields[i];
        mapping[thisField.FieldName] = "#" + thisField.ValidationMessageId;
    }

    return mapping;
}

function __MVC_CreateErrorMessagesObject(validationFields) {
    var messagesObj = {};

    for (var i = 0; i < validationFields.length; i++) {
        var thisField = validationFields[i];
        var thisFieldMessages = {};
        messagesObj[thisField.FieldName] = thisFieldMessages;
        var validationRules = thisField.ValidationRules;

        for (var j = 0; j < validationRules.length; j++) {
            var thisRule = validationRules[j];
            if (thisRule.ErrorMessage) {
                var jQueryValidationType = thisRule.ValidationType;
                switch (thisRule.ValidationType) {
                    case "regularExpression":
                        jQueryValidationType = "regex";
                        break;

                    case "stringLength":
                        jQueryValidationType = "maxlength";
                        break;
                }

                thisFieldMessages[jQueryValidationType] = thisRule.ErrorMessage;
            }
        }
    }

    return messagesObj;
}

function __MVC_CreateRulesForField(validationField) {
    var validationRules = validationField.ValidationRules;

    // hook each rule into jquery
    var rulesObj = {};
    for (var i = 0; i < validationRules.length; i++) {
        var thisRule = validationRules[i];
        switch (thisRule.ValidationType) {
            case "range":
                __MVC_ApplyValidator_Range(rulesObj,
                    thisRule.ValidationParameters["minimum"], thisRule.ValidationParameters["maximum"]);
                break;

            case "regularExpression":
                __MVC_ApplyValidator_RegularExpression(rulesObj,
                    thisRule.ValidationParameters["pattern"]);
                break;

            case "required":
                var fieldName = validationField.FieldName.replace(".", "_");
                if ($("#" + fieldName).get(0).type !== 'checkbox') {
                    // only apply required if the input control is NOT a checkbox.
                    __MVC_ApplyValidator_Required(rulesObj);
                }
                break;

            case "stringLength":
                __MVC_ApplyValidator_StringLength(rulesObj,
                    thisRule.ValidationParameters["maximumLength"]);
                break;

            default:
                __MVC_ApplyValidator_Unknown(rulesObj,
                    thisRule.ValidationType, thisRule.ValidationParameters);
                break;
        }
    }

    return rulesObj;
}

function __MVC_CreateValidationOptions(validationFields) {
    var rulesObj = {};
    for (var i = 0; i < validationFields.length; i++) {
        var validationField = validationFields[i];
        var fieldName = validationField.FieldName;
        rulesObj[fieldName] = __MVC_CreateRulesForField(validationField);
    }

    return rulesObj;
}

function __MVC_EnableClientValidation(validationContext) {
    // this represents the form containing elements to be validated
    var theForm = $("#" + validationContext.FormId);

    var fields = validationContext.Fields;
    var rulesObj = __MVC_CreateValidationOptions(fields);
    var fieldToMessageMappings = __MVC_CreateFieldToValidationMessageMapping(fields);
    var errorMessagesObj = __MVC_CreateErrorMessagesObject(fields);

    var options = {
        errorClass: "input-validation-error",
        errorElement: "span",
        errorPlacement: function(error, element) {
            var messageSpan = fieldToMessageMappings[element.attr("name")];
            $(messageSpan).empty();
            $(messageSpan).removeClass("field-validation-valid");
            $(messageSpan).addClass("field-validation-error");
            error.removeClass("input-validation-error");
            error.attr("_for_validation_message", messageSpan);
            error.appendTo(messageSpan);
        },
        messages: errorMessagesObj,
        rules: rulesObj,
        success: function(label) {
            var messageSpan = $(label.attr("_for_validation_message"));
            $(messageSpan).empty();
            $(messageSpan).addClass("field-validation-valid");
            $(messageSpan).removeClass("field-validation-error");
        }
    };

    var validationSummaryId = validationContext.ValidationSummaryId;
    if (validationSummaryId) {
        // insert an empty <ul> into the validation summary <div> tag (as necessary)
        $("<ul />").appendTo($("#" + validationSummaryId + ":not(:has(ul:first))"));

        options = {
            errorContainer: "#" + validationSummaryId,
            errorLabelContainer: "#" + validationSummaryId + " ul:first",
            wrapper: "li",

            showErrors: function(errorMap, errorList) {
                var errContainer = $(this.settings.errorContainer);
                var errLabelContainer = $("ul:first", errContainer);

                // Add error CSS class to user-input controls with errors
                for (var i = 0; this.errorList[i]; i++) {
                    var element = this.errorList[i].element;
                    var messageSpan = $(fieldToMessageMappings[element.name]);
                    var msgSpanHtml = messageSpan.html();
                    if (!msgSpanHtml || msgSpanHtml.length == 0) {
                        // Don't override the existing Validation Message.
                        // Only if it is empty, set it to an asterisk.
                        //messageSpan.html("*");
                    }
                    messageSpan.removeClass("field-validation-valid").addClass("field-validation-error");
                    $("#" + element.id).addClass("input-validation-error");
                }
                for (var i = 0; this.successList[i]; i++) {
                    // Remove error CSS class from user-input controls with zero validation errors
                    var element = this.successList[i];
                    var messageSpan = fieldToMessageMappings[element.name];
                    $(messageSpan).addClass("field-validation-valid").removeClass("field-validation-error");
                    $("#" + element.id).removeClass("input-validation-error");
                }

                if (this.numberOfInvalids() > 0) {
                    errContainer.removeClass("validation-summary-valid").addClass("validation-summary-errors");
                }

                this.defaultShowErrors();

                // when server-side errors still exist in the Validation Summary, don't hide it
                var totalErrorCount = errLabelContainer.children("li:not(:has(label))").length + this.numberOfInvalids();
                if (totalErrorCount > 0) {
                    $(this.settings.errorContainer).css("display", "block").addClass("validation-summary-errors").removeClass("validation-summary-valid");
                    $(this.settings.errorLabelContainer).css("display", "block");
                }
            },
            messages: errorMessagesObj,
            rules: rulesObj
        };
    }

    // register callbacks with our AJAX system
    var formElement = document.getElementById(validationContext.FormId);
    var registeredValidatorCallbacks = formElement.validationCallbacks;
    if (!registeredValidatorCallbacks) {
        registeredValidatorCallbacks = [];
        formElement.validationCallbacks = registeredValidatorCallbacks;
    }
    registeredValidatorCallbacks.push(function() {
        theForm.validate();
        return theForm.valid();
    });

    theForm.validate(options);
}

// need to wait for the document to signal that it is ready
$(document).ready(function() {
    var allFormOptions = window.mvcClientValidationMetadata;
    if (allFormOptions) {
        while (allFormOptions.length > 0) {
            var thisFormOptions = allFormOptions.pop();
            __MVC_EnableClientValidation(thisFormOptions);
        }
    }
}); 

I've tried moving the calls at the bottom in the document ready into my OnSuccess method, but that didn't do it.

So, how do I get client side validation to reinitialize when I do my ajax replace, and how do I get all my errors to show in the validation summary? I'm hoping that if I fix one issue, it will correct the other.

EDIT:

Here's a little more info about what I am doing

Here's the wrapper

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<QuickPayModel>" %>

<div id="quickpay-wrapper">
<% if (Model.NewPaymentMethod) { %>
    <% Html.RenderAction<DashboardController>(x => x.QuickPayNewMethod()); %>
<% } else { %>
    <% Html.RenderPartial("QuickPayMakePayment", Model); %>
<% } %>
</div>

Here is the make a payment panel.

<%= Html.ClientValidationSummary(new { id = "valSumContainer" })%>
<% Html.EnableClientValidation(); %>

<% using (Ajax.BeginForm("QuickPay", "Dashboard",
       new { },
       new AjaxOptions() { 
           HttpMethod = "Post",
           InsertionMode = InsertionMode.Replace,
           UpdateTargetId = "quickpay-wrapper",
           OnSuccess = "updatePaymentHistory",
           LoadingElementId = "loading-pane"
            }, new { }))
   { %>
    <div class="horizontalline"><%= Html.Spacer() %></div>
    <% ViewContext.FormContext.ValidationSummaryId = "valSumContainer"; %>

    <p>
    <%: Html.LabelFor(x => x.PaymentMethods)%>

    <% if (Model.HasOnePaymentMethod) { %>
            <%: Html.DisplayFor(x => x.SelectedPaymentMethodName) %>
            <%: Html.HiddenFor(x => x.SelectedPaymentMethodId) %>

    <% } else { %>
        <%: Html.DropDownListFor(x => x.SelectedPaymentMethodId, Model.PaymentMethodsSelectList, "Select a Payment Method", new { })%>
            <%: Html.HiddenFor(x => x.SelectedPaymentMethodName)%>
        <script type="text/javascript">
            $(function () {
                $("#PaymentMethods").change(function () {
                    $("#SelectedPaymentMethodId").val($(this).val());

                    $("#SelectedPaymentMethodName").val($('option:selected', this).text());
                });
            });
        </script>

    <% } %>
    <%: Html.Spacer(12, 1) %><%: Ajax.ActionLink("New Payment Method", "QuickPayNewMethod", 
                                 new AjaxOptions() { InsertionMode = InsertionMode.Replace,
                                                     UpdateTargetId = "quickpay-wrapper",
                                                     OnSuccess = "newPaymentSetup",
                                                     LoadingElementId = "loading-pane"
                                 })%> 
    <%: Html.ValidationMessageFor(x => x.SelectedPaymentMethodId)%>

    </p>

    <p>
    <%: Html.LabelFor(x => x.Amount)%>
    <%: Html.TextBoxFor(x => x.Amount, new { disabled = Model.UseInvoicing ? "disabled" : String.Empty, 
    title = Model.UseInvoicing ? "the total payment amount of all selected invoices" : String.Empty,
    @class = "small" })%>
    <%: Html.ValidationMessageFor(x => x.Amount)%>
    </p>

    <p>
    <%: Html.LabelFor(x => x.PayDate)%>
    <%: Html.TextBox("PayDate", Model.PayDate.ToShortDateString(), new { @class = "medium" })%>
    <%: Html.ValidationMessageFor(x => x.PayDate)%>
    </p>

    <script type="text/javascript">
        $(function () {
            quickPaySetup();
        });
    </script>

    <div class="horizontalline"><%= Html.Spacer() %></div>
    <%= FTNI.Controls.Submit("Submit Payment") %>
    <%: Html.AntiForgeryToken() %>

    <%: Html.ValidationMessage("Payment-Result")%>
<% } %>

And now my new payment method panel

<script type="text/javascript">
    $(function () {
        newPaymentSetup();
    });
    </script>

    <h4>New Payment Method</h4> 

    <% if(Model.HasPaymentMethods) { %>
        <span style="float:right;">
            <%: Ajax.ActionLink("Cancel", "QuickPay", 
                                 new AjaxOptions() { 
                                     HttpMethod = "Get",
                                     InsertionMode = InsertionMode.Replace,
                                     UpdateTargetId = "quickpay-wrapper",
                                     OnSuccess = "quickPaySetup",
                                     LoadingElementId = "loading-pane"
                                 })%>
        </span>
    <% } %>

    <div>Enter the information below to create a new payment method.</div><br />

    <%= Html.ClientValidationSummary(new { id = "valSumContainer" })%>
    <% Html.EnableClientValidation(); %>
<div id="new-payment-method-tabs">
    <ul>
        <li><a href="#new-credit-card">Credit Card</a></li>
        <li><a href="#new-ach">E-Check</a></li>
    </ul>
    <div id="new-credit-card">
        <% Html.RenderPartial("NewCreditCard", Model.CreditCardModel); %>
    </div>
    <div id="new-ach">
        <% Html.RenderPartial("NewACH", Model.ACHModel); %>
    </div>
</div>

Each form starts off with something like this

<% using (Ajax.BeginForm("AddCreditCard", "Dashboard",
       new { },
       new AjaxOptions() { 
           HttpMethod = "Post",
           InsertionMode = InsertionMode.Replace,
           UpdateTargetId = "quickpay-wrapper",
           OnSuccess = "newPaymentSetup",
           LoadingElementId = "loading-pane"
            }, new { id="new-credit-card-form" })) { %>
    <% ViewContext.FormContext.ValidationSummaryId = "valSumContainer"; %>

Initial load works. Any ajax replaces cause the form context to be lost and not reinitialize no matter what I do. The form posts back, validation occurs server side. All invalid fields are changed (css error classes added), but only the last error is shown in the summary.

回答1:

I am going to tell you how I did just a few days ago. Please have a look to this question for details.

In my case I was showing the content of the form using an ajax call inside a jquery dialog. When the call complete I just replace the dialog content with the content sent back from the controller.

I have modified the code inside the Microsoft script as described in my question and then called the init method on document ready. This should work for your case too...

For the second error (how do I get all my errors to show in the validation summary?) I have simply modified the code as described in the same source post you are referring to and did not experienced any problem.

Hope it helps!



回答2:

I ended up reworking my solution so that I was no longer writing the forms to the page through callbacks. While this was not my intended approach, it does work. I chose to use jquery modals to display the data rather than changing the content of one area of the screen.

Ideally, I would not have to render all of the forms to the page and could call them up on demand, but it seems jquery client side validation will not wire up unless the form is present on the page load. I am unsure of the form elements are required present on form load, but it may be a limitation I just have to deal with.

Another workaround would have been to render them all to the page and just show/hide each form through jquery. It's not much different than using modals, but at least the validation would work. I'd still like to see a solution where forms using validation summaries and client side jquery validation through data annotations can be written to the page through callbacks and still wired up and function correctly.