MVC4 Validation with razor and twitter bootstrap

2019-01-26 00:12发布

问题:

I need to validate my input in MVC4 razor. The problem is, I want to use twitter bootstrap and then set the error message class for the top div as seen here: http://getbootstrap.com/css/#forms-control-states

How can I change the parent div's class?

回答1:

Have you tried this script?

http://blog.ijasoneverett.com/2013/05/asp-net-mvc-validation-with-twitters-bootstrap/

I use something similar for getting bootstrap and mvc validation to play nice



回答2:

The accepted answer needed modifications to work with Bootstrap 3. I have used the following with success with MVC 4 and Bootstrap 3.

$(function () {
 // any validation summary items should be encapsulated by a class alert and alert-danger
    $('.validation-summary-errors').each(function () {
        $(this).addClass('alert');
        $(this).addClass('alert-danger');
    });

    // update validation fields on submission of form
    $('form').submit(function () {
        if ($(this).valid()) {
            $(this).find('div.control-group').each(function () {
                if ($(this).find('span.field-validation-error').length == 0) {
                    $(this).removeClass('has-error');
                    $(this).addClass('has-success');
                }
            });
        }
        else {
            $(this).find('div.control-group').each(function () {
                if ($(this).find('span.field-validation-error').length > 0) {
                    $(this).removeClass('has-success');
                    $(this).addClass('has-error');
                }
            });
            $('.validation-summary-errors').each(function () {
                if ($(this).hasClass('alert-danger') == false) {
                    $(this).addClass('alert');
                    $(this).addClass('alert-danger');
                }
            });
        }
    });

    // check each form-group for errors on ready
    $('form').each(function () {
        $(this).find('div.form-group').each(function () {
            if ($(this).find('span.field-validation-error').length > 0) {
                $(this).addClass('has-error');
            }
        });
    });
});

var page = function () {
    //Update the validator
    $.validator.setDefaults({
        highlight: function (element) {
            $(element).closest(".form-group").addClass("has-error");
            $(element).closest(".form-group").removeClass("has-success");
        },
        unhighlight: function (element) {
            $(element).closest(".form-group").removeClass("has-error");
            $(element).closest(".form-group").addClass("has-success");
        }
    });
}();


回答3:

Modify validator defaults:

// Override jquery validate plugin defaults in order to utilize Twitter Bootstrap 3 has-error, has-success, etc. styling.
$.validator.setDefaults({
    highlight: function(element) {
        $(element).closest('.form-group').addClass('has-error').removeClass('has-success');
    },
    unhighlight: function(element) {
        $(element).closest('.form-group').removeClass('has-error').addClass('has-success');
    },
    errorElement: 'span',
    errorClass: 'help-block',
    errorPlacement: function (error, element) {
        if (element.parent('.input-group').length || element.prop('type') === 'checkbox' || element.prop('type') === 'radio') {
            error.insertAfter(element.parent());
        } else {
            error.insertAfter(element);
        }
    }
});

Create a new validation summary partial (so you have complete control over markup):

@model ModelStateDictionary

<div class="@(Html.ViewData.ModelState.IsValid ? "validation-summary-valid" : "validation-summary-errors") panel panel-danger" data-valmsg-summary="true">
    <div class="panel-heading">
        Please, correct the following errors:
    </div>
    <div class="panel-body">
        <ul>
            @foreach (var modelError in Model.SelectMany(keyValuePair => keyValuePair.Value.Errors))
            {
                <li>@modelError.ErrorMessage</li>
            }
        </ul>
    </div>
</div>

Add one style to your CSS (to hide the validation summary on initial load without needing to depend on client-side scripting to hide it after the fact):

.validation-summary-valid { 
    display: none; 
}

And render your alternative validation summary in your view instead of @Html.ValidationSummary():

   @{Html.RenderPartial("_HtmlValidationSummary", ViewData.ModelState);}

And you're good to go. Oh, and to get the nice styling for checkbox and radio, depending on your setup the above script will catch it, but the easiest is to simply modify your i.e. Boolean.cshtml partial to be like the following (where Html.FormGroupFor is just a slightly updated version of https://github.com/erichexter/twitter.bootstrap.mvc/blob/master/src/Bootstrap/BootstrapSupport/ControlGroupExtensions.cs - just change "control-group" references to "form-group" and add " controlGroupWrapper.AddCssClass("has-error");" on line 58'ish):

@using Bctf.Web.HtmlHelpers
@model bool?

@using (Html.FormGroupFor(x => Model))
{
    <label class="checkbox">
        @Html.CheckBox("", Model.HasValue && Model.Value, new { @class = "tickbox-single-line" }) @ViewData.ModelMetadata.DisplayName
    </label>
    @Html.ValidationMessageFor(x => Model, string.Empty, new { @class = "help-block" })
}

All of the above is compilation of various solutions I've found from many different locations that have made the right mix. Ben's solution above works, but it does a fair bit client-side which causes a jitter whenever some method in it is called (i.e. post, page load, validation error). The above does all that you need without having all that DOM looping.

The various answers found at Bootstrap 3 with jQuery Validation Plugin cover much of the above (and credit for a significant chunk of the above goes to the various posters there, although no one answer covers it all).



回答4:

Solution

Include last:

$.validator.unobtrusive.options = {
    errorPlacement: function (error, element) {        
        $(element).closest(".form-group").addClass("has-error");
    }
    , success: function (label, element) {
        $(element).closest(".form-group").removeClass("has-error");        
    }
}

Currently in jquery.validate.unobtrusive.js the following code shows how MS allows for extending itself:

options: {  // options structure passed to jQuery Validate's validate() method
    errorClass: defaultOptions.errorClass || "input-validation-error",
    errorElement: defaultOptions.errorElement || "span",
    errorPlacement: function () {
        onError.apply(form, arguments);
        execInContext("errorPlacement", arguments);
    },
    invalidHandler: function () {
        onErrors.apply(form, arguments);
        execInContext("invalidHandler", arguments);
    },
    messages: {},
    rules: {},
    success: function () {
        onSuccess.apply(form, arguments);
        execInContext("success", arguments);
    }
}

Allows you to basically override/extend the following options:

  • errorClass
  • errorElement
  • errrorPlacement
  • invalidHandler
  • success

It does this by looking at the global variable $.validator.unobtrusive.options (which doesn't exist unless you make it). defaultOptions is set to $.validator.unobtrusive.options and execInContext is a method that finds the applicable method in $.validator.unobtrusive.options and executes it with the original arguments (so that the original MS method doesn't get clobbered).



回答5:

I like to add class on server side, and not client side, so, my solution has no javascript (in my case, it's a simple system with only 2 or 3 fields on the form).

This is to use the same structure from Form -> Validation States from Bootstrap documentation, available in http://getbootstrap.com/css/#forms-control-validation

First, I created a Html Helper to check if the ModelState is valid, and if has a error on the field, return a string (class name). I could have done this directly on the view, but I like a cleaner view. So, my class:

public static class ErrorHelper
{
    private static string GetPropertyName<TModel, TValue>(Expression<Func<TModel, TValue>> expression)
    {
        // Get field name
        // Code from: https://stackoverflow.com/a/2916344/4794469
        MemberExpression body = expression.Body as MemberExpression;
        if (body != null) return body.Member.Name;

        UnaryExpression ubody = expression.Body as UnaryExpression;
        if (ubody != null) body = ubody.Operand as MemberExpression;

        return body?.Member.Name;
    }

    public static MvcHtmlString ReturnOnError<TModel, TValue>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression, string @class)
    {
        var propertyName = GetPropertyName(expression);
        if (propertyName == null)
            return MvcHtmlString.Empty;

        if (htmlHelper.ViewData.ModelState.IsValid || !htmlHelper.ViewData.ModelState.ContainsKey(propertyName))
            return MvcHtmlString.Empty;

        return htmlHelper.ViewData.ModelState[propertyName].Errors.Any()
            ? MvcHtmlString.Create(@class)
            : MvcHtmlString.Empty;
    }
}

Imports:

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Web.Mvc;

Now, in my View, I just need to use the class, like:

<div class="form-group @Html.ReturnOnError(m => m.Name, "has-error")">
    @Html.LabelFor(m => m.Name, new { @class = "control-label" })
    @Html.TextBoxFor(m => m.Name, new { @class = "form-control" })
    @Html.ValidationMessageFor(m => m.Name, null, new { @class = "help-block" })
</div>