3

I have a form with client side validation, and when an error input is detected, a class attribute is changed of an input field; It is changed to include "input-validation-error" class.

I want to change this class, in order not to use it but instead use Bootstraps class "is-invalid".

I tried using ASP.NET Core's TagHelpers, but this has no effect; I believe that this will not work as the helpers will only work if the "whole page" is loaded, it does not help with client side validation.

When I search in the .NET project one finds the css class defined in, the "Unobtrusive validation support library for jQuery".

What is the best way to change this class?

Could CSS help by changing a class from one to the other? (overriding the original class, not sure if this is possible)

Or should one use JavaScript to reconfigure JQuery?

Here is my TagHelper, adding the helpers: validation-for,validation-error-class,validation-valid-class

The Form/Html...

<input type="email" asp-for="Email" id="inputEmail" class="form-control" placeholder="Email address" required
                            validation-for="Email" validation-error-class="is-invalid" validation-valid-class="is-valid"/>
                            <span class="small" asp-validation-for="Email"></span>

Here is a snippet of the code for my TagHelper.

[HtmlTargetElement("input", Attributes = "validation-for,validation-error-class,validation-valid-class")]
public class ValidationErrorClassTagHelper : TagHelper
{
    [HtmlAttributeName("validation-for")]
    public ModelExpression For { get; set; }

    [HtmlAttributeName("validation-error-class")]
    public string ErrorClass { get; set; }
    [HtmlAttributeName("validation-valid-class")]
    public string ValidClass { get; set; }


    [HtmlAttributeNotBound]
    [ViewContext]
    public ViewContext ViewContext { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.RemoveClass(ErrorClass,HtmlEncoder.Default);
        output.RemoveClass(ValidClass,HtmlEncoder.Default);

        if (ViewContext.ViewData.ModelState.IsValid) {                
            output.AddClass(ValidClass,HtmlEncoder.Default);
        } else 
        {
            output.AddClass(ErrorClass,HtmlEncoder.Default);                
        }          
    }
}

New Approach not working 100%.

I have tried an alternative approach, by modifying the jQuery defaultOptions, changing the errorClass and the validClass.

Snippet of the Code found here on [https://github.com/brecons/jquery-validation-unobtrusive-bootstrap][gitHub]

function ($) {
    if($.validator && $.validator.unobtrusive){
        var defaultOptions = {
            validClass: 'is-valid',
            errorClass: 'is-invalid',

This works for the errorClass, but for me the validClass remains unchanged, it remains to be named valid.

Wayne
  • 3,359
  • 3
  • 30
  • 50

3 Answers3

7

I have encountered this issue too but I don't really like the idea of fixing it at client-side as the jQuery-trick works only when JavaScript is enabled in the browser. Because of this, I think the problem should be addressed at server-side.

Unfortunately, the framework doesn't provide a way to configure the validation-related css classes, these strings are simply hard-coded.

However, these fields are not constants but declared as static, so we may change their value at run-time via reflection (somehow like presented here). But these kinds of things are dirty hacks which should be our last resort.

OP's idea of a custom tag helper looks much better. But it has a shortcoming: it only fixes markup generated by tag helpers. The classic, Html.TextBox(...)-like approach would be still broken.

So, can we do any better? Luckily, yes!

Both Html and tag helper implementations use the IHtmlGenerator service under the hood to generate their markup. Thanks to the modular architecture of ASP.NET Core, we can provide a customized version of this service. (What's more, we can even do this without copying a bunch of code as the default implementation declares the relevant methods as virtual.)

Thus, I could come up with this solution:

public sealed class CustomHtmlGenerator : DefaultHtmlGenerator
{
    private static IHtmlHelper GetHtmlHelperFor(ViewContext viewContext)
    {
        const string htmlHelperViewDataKey = nameof(CustomHtmlGenerator) + "_" + nameof(IHtmlHelper);

        if (!viewContext.ViewData.TryGetValue(htmlHelperViewDataKey, out var htmlHelperObj) || !(htmlHelperObj is IHtmlHelper htmlHelper))
            viewContext.ViewData[htmlHelperViewDataKey] = htmlHelper = GetViewHtmlHelper(viewContext) ?? CreateHtmlHelper(viewContext);

        return htmlHelper;

        static IHtmlHelper GetViewHtmlHelper(ViewContext viewContext)
        {
            if (!(viewContext.View is RazorView razorView))
                return null;

            dynamic razorPage = razorView.RazorPage;

            try { return (IHtmlHelper)razorPage.Html; }
            catch { return null; }
        }

        static IHtmlHelper CreateHtmlHelper(ViewContext viewContext)
        {
            var htmlHelper = viewContext.HttpContext.RequestServices.GetRequiredService<IHtmlHelper>();
            (htmlHelper as IViewContextAware)?.Contextualize(viewContext);
            return htmlHelper;
        }
    }

    private static TagBuilder AddBootstrapValidationCssClasses(ViewContext viewContext, string expression, TagBuilder tagBuilder)
    {
        // we need to get the model property key from the expression, which functionality is buried in an internal class unfortunately
        // (https://github.com/dotnet/aspnetcore/blob/v3.1.6/src/Mvc/Mvc.ViewFeatures/src/NameAndIdProvider.cs#L147)
        // however, this internal API is exposed via the IHtmlHelper.Name method:
        // (https://github.com/dotnet/aspnetcore/blob/v3.1.6/src/Mvc/Mvc.ViewFeatures/src/HtmlHelper.cs#L451)
        var htmlHelper = GetHtmlHelperFor(viewContext);
        var fullName = htmlHelper.Name(expression);

        if (viewContext.ModelState.TryGetValue(fullName, out var entry))
        {
            if (entry.ValidationState == ModelValidationState.Invalid)
                tagBuilder.AddCssClass("is-invalid");
            else if (entry.ValidationState == ModelValidationState.Valid)
                tagBuilder.AddCssClass("is-valid");
        }

        return tagBuilder;
    }

    public CustomHtmlGenerator(IAntiforgery antiforgery, IOptions<MvcViewOptions> optionsAccessor, IModelMetadataProvider metadataProvider, IUrlHelperFactory urlHelperFactory, HtmlEncoder htmlEncoder, ValidationHtmlAttributeProvider validationAttributeProvider)
        : base(antiforgery, optionsAccessor, metadataProvider, urlHelperFactory, htmlEncoder, validationAttributeProvider) { }

    protected override TagBuilder GenerateInput(ViewContext viewContext, InputType inputType, ModelExplorer modelExplorer, string expression, object value, bool useViewData, bool isChecked, bool setId, bool isExplicitValue, string format, IDictionary<string, object> htmlAttributes) =>
        AddBootstrapValidationCssClasses(viewContext, expression, base.GenerateInput(viewContext, inputType, modelExplorer, expression, value, useViewData, isChecked, setId, isExplicitValue, format, htmlAttributes));

    public override TagBuilder GenerateSelect(ViewContext viewContext, ModelExplorer modelExplorer, string optionLabel, string expression, IEnumerable<SelectListItem> selectList, ICollection<string> currentValues, bool allowMultiple, object htmlAttributes) =>
        AddBootstrapValidationCssClasses(viewContext, expression, base.GenerateSelect(viewContext, modelExplorer, optionLabel, expression, selectList, currentValues, allowMultiple, htmlAttributes));

    public override TagBuilder GenerateTextArea(ViewContext viewContext, ModelExplorer modelExplorer, string expression, int rows, int columns, object htmlAttributes) =>
        AddBootstrapValidationCssClasses(viewContext, expression, base.GenerateTextArea(viewContext, modelExplorer, expression, rows, columns, htmlAttributes));
}

All that is left is to configure DI to resolve this custom implementation by adding the following at the end of the Startup.ConfigureService method:

services.Replace(ServiceDescriptor.Singleton<IHtmlGenerator, CustomHtmlGenerator>());
Adam Simon
  • 2,762
  • 16
  • 22
3

I know this has been posted for a while.

ASP.NET Core MVC can use JQuery validation with unobtrusive script Tutorial here for data annotations validation, customize the validator object and configure the validClass and errorClass properties, like this:

<script>
        
var settings = {
        validClass: "is-valid",
        errorClass: "is-invalid"
    
    }; 
    $.validator.setDefaults(settings);
    $.validator.unobtrusive.options = settings;
</script>

You can place the above code snippet in the _ValidationScriptsPartial.cshtml file. And now if you run your application, you will be able to see the Bootstrap validation style messages.

Marcus
  • 453
  • 3
  • 10
  • This does change the client side validation classes but does not change the css class populated via the tag helper from server side validation. See the answer from Adam Simon for server side changes. – Steven Quick Aug 24 '20 at 02:02
  • The simplest approach; anything else is a waste of time. See [here](https://dotnetthoughts.net/how-to-use-bootstrap-style-validation-in-aspnet-core/) for before/after pictures. – lonix Feb 23 '23 at 06:47
-2

Found a simple solution! Add a CSS change listener to the input element; This example adds a listener to the "input" element, but could also be applied to other elements.

Taken from here jQuery - Fire event if CSS class changed

Here is my implementation, notice that I modify the CSS in the listener, which could potentially cause a stack overflow.

<script type="text/javascript">
// Create a closure
(function() {

    var originalAddClassMethod = jQuery.fn.addClass;

    jQuery.fn.addClass = function() {
        // Execute the original method.
        var result = originalAddClassMethod.apply(this, arguments);

        // trigger a custom event
        // stop recursion...
        if ((arguments[0] != "is-valid") && (arguments[0] != "is-invalid")) {
            jQuery(this).trigger('cssClassChanged');
        }            

        // return the original result
        return result;
    }
})();

// document ready function
$(function() {
    $("input").bind('cssClassChanged', function(){ 
        // cleanup
        if ($(this).hasClass("is-valid")) {
            $(this).removeClass("is-valid");
        }
        if ($(this).hasClass("is-invalid")) {
            $(this).removeClass("is-invalid");
        }

        // remap the css classes to that of BootStrap 
        if ($(this).hasClass("input-validation-error")) {
            $(this).addClass("is-invalid");        
        }

        if ($(this).hasClass("valid")) {
            $(this).addClass("is-valid");        
        }
    });
});
</script>
Wayne
  • 3,359
  • 3
  • 30
  • 50