-
-
Save FWest98/9141b3c2f260bee0e46058897d2017d2 to your computer and use it in GitHub Desktop.
// This is the default partial generated in the template, if you have another place | |
// where you include the jQuery unobtrusive validation plugin, put this code there | |
@using Foo | |
@inject IOptions<ValidationOptions> validationOptionsHolder | |
@{ var validationOptions = validationOptionsHolder.Value; } | |
// Here is your normal environment stuff | |
<script> | |
(function () { | |
// Set default options for the validation | |
const defaultOptions = { | |
errorClass: "@validationOptions.InvalidInput", // or add custom, extra ones | |
validClass: "@validationOptions.ValidInput" | |
}; | |
$.validator.setDefaults(defaultOptions); | |
$.validator.unobtrusive.options = defaultOptions; | |
})(); | |
</script> |
using System.Collections.Generic; | |
using System.Text.Encodings.Web; | |
using Microsoft.AspNetCore.Antiforgery; | |
using Microsoft.AspNetCore.Mvc; | |
using Microsoft.AspNetCore.Mvc.ModelBinding; | |
using Microsoft.AspNetCore.Mvc.Rendering; | |
using Microsoft.AspNetCore.Mvc.Routing; | |
using Microsoft.AspNetCore.Mvc.ViewFeatures; | |
using Microsoft.Extensions.Options; | |
namespace Foo | |
{ | |
// Custom HTML Generator to override the default behaviour and replace the CSS classes as they emerge | |
public class CustomHtmlGenerator : DefaultHtmlGenerator | |
{ | |
private readonly ValidationOptions _options; | |
public CustomHtmlGenerator(IAntiforgery antiforgery, IOptions<MvcViewOptions> optionsAccessor, | |
IModelMetadataProvider metadataProvider, IUrlHelperFactory urlHelperFactory, HtmlEncoder htmlEncoder, | |
ValidationHtmlAttributeProvider validationAttributeProvider, IOptions<ValidationOptions> options) : | |
base(antiforgery, optionsAccessor, metadataProvider, urlHelperFactory, htmlEncoder, validationAttributeProvider) | |
{ | |
_options = options.Value; | |
} | |
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) | |
{ | |
var tagBuilder = base.GenerateInput(viewContext, inputType, modelExplorer, expression, value, useViewData, isChecked, setId, isExplicitValue, format, htmlAttributes); | |
FixValidationCssClassNames(tagBuilder); | |
return tagBuilder; | |
} | |
public override TagBuilder GenerateTextArea(ViewContext viewContext, ModelExplorer modelExplorer, string expression, int rows, | |
int columns, object htmlAttributes) | |
{ | |
var tagBuilder = base.GenerateTextArea(viewContext, modelExplorer, expression, rows, columns, htmlAttributes); | |
FixValidationCssClassNames(tagBuilder); | |
return tagBuilder; | |
} | |
public override TagBuilder GenerateValidationMessage(ViewContext viewContext, ModelExplorer modelExplorer, string expression, | |
string message, string tag, object htmlAttributes) | |
{ | |
var tagBuilder = base.GenerateValidationMessage(viewContext, modelExplorer, expression, message, tag, htmlAttributes); | |
FixValidationCssClassNames(tagBuilder); | |
return tagBuilder; | |
} | |
public override TagBuilder GenerateValidationSummary(ViewContext viewContext, bool excludePropertyErrors, string message, | |
string headerTag, object htmlAttributes) | |
{ | |
var tagBuilder = base.GenerateValidationSummary(viewContext, excludePropertyErrors, message, headerTag, htmlAttributes); | |
FixValidationCssClassNames(tagBuilder); | |
return tagBuilder; | |
} | |
private void FixValidationCssClassNames(TagBuilder tagBuilder) | |
{ | |
tagBuilder.ReplaceCssClass(HtmlHelper.ValidationInputCssClassName, _options.InvalidInput); | |
tagBuilder.ReplaceCssClass(HtmlHelper.ValidationInputValidCssClassName, _options.ValidInput); | |
tagBuilder.ReplaceCssClass(HtmlHelper.ValidationMessageCssClassName, _options.InvalidMessage); | |
tagBuilder.ReplaceCssClass(HtmlHelper.ValidationMessageValidCssClassName, _options.ValidMessage); | |
tagBuilder.ReplaceCssClass(HtmlHelper.ValidationSummaryCssClassName, _options.InvalidSummary); | |
tagBuilder.ReplaceCssClass(HtmlHelper.ValidationSummaryValidCssClassName, _options.ValidSummary); | |
} | |
} | |
public static class TagBuilderHelpers | |
{ | |
public static void ReplaceCssClass(this TagBuilder tagBuilder, string old, string val) | |
{ | |
if (!tagBuilder.Attributes.TryGetValue("class", out string str)) return; | |
tagBuilder.Attributes["class"] = str.Replace(old, val); | |
} | |
} | |
} |
using System; | |
using System.DirectoryServices.AccountManagement; | |
using System.Linq; | |
using Microsoft.AspNetCore.Authentication.Cookies; | |
using Microsoft.AspNetCore.Authentication.WsFederation; | |
using Microsoft.AspNetCore.Builder; | |
using Microsoft.AspNetCore.Hosting; | |
using Microsoft.AspNetCore.Mvc.DataAnnotations; | |
using Microsoft.AspNetCore.Mvc.ViewFeatures; | |
using Microsoft.EntityFrameworkCore; | |
using Microsoft.Extensions.Configuration; | |
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.Extensions.Hosting; | |
using Microsoft.Extensions.Options; | |
namespace Foo | |
{ | |
public class Startup | |
{ | |
public Startup(IConfiguration configuration) | |
{ | |
Configuration = configuration; | |
} | |
public IConfiguration Configuration { get; } | |
// This method gets called by the runtime. Use this method to add services to the container. | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
// All your usual stuff | |
// Configure Options | |
services.Configure<ValidationOptions>(Configuration.GetSection("Validation")); // or use a custom source | |
// Configure custom html generator to override css classnames | |
services.AddSingleton<IHtmlGenerator, CustomHtmlGenerator>(); | |
} | |
} | |
} |
namespace Foo | |
{ | |
// Options class to set the validation classes. Of course, this is not required as you can | |
// also harcode the classnames in the CustomHtmlGenerator file. | |
public class ValidationOptions | |
{ | |
public string ValidInput { get; set; } | |
public string InvalidInput { get; set; } | |
public string InvalidMessage { get; set; } | |
public string ValidMessage { get; set; } | |
public string InvalidSummary { get; set; } | |
public string ValidSummary { get; set; } | |
} | |
} |
I am quite sure it still works on .NET 6, but I developed it for .NET Core 3.1 and then used it for .NET 5. I indeed use bootstrap (but an older version I believe) with the following part in my appsettings.json
:
"Validation": {
"ValidInput": "is-valid",
"InvalidInput": "is-invalid",
"InvalidMessage": "invalid-feedback",
"ValidMessage": "valid-feedback"
}
Thank you!
Fantastic fix, thank you!
This is great, thanks! Is there any way we can control InvalidMessage / ValidMessage from the client side? I see that it ValidInput / InvalidInput works great (client and server), but InvalidMessage / ValidMessage seems to be server only and the client-side validator is still adding the 'field-validation-error' / 'field-validation-valid' classes. Thanks again!
I think you'll have to dig into the inner working of the validator then... Not sure how that could be done.
I ultimately figured it out... the values are hard-coded in jquery.validate.unobtrusive.js so a direct change is required. I added a couple of getter methods to pull the class name from the options from '$jQval.unobtrusive.options', then replaced all the hard coded values with calls to the getter functions. Fingers crossed MS does something better in .NET 8!
Thanks again!
function getFieldValidationErrorCss() {
if ($jQval.unobtrusive && $jQval.unobtrusive.options) {
if ($jQval.unobtrusive.options.fieldInvalidClass)
return $jQval.unobtrusive.options.fieldInvalidClass;
}
return "field-validation-error";
}
function getFieldValidationValidCss() {
if ($jQval.unobtrusive && $jQval.unobtrusive.options) {
if ($jQval.unobtrusive.options.fieldValidClass)
return $jQval.unobtrusive.options.fieldValidClass;
}
return "field-validation-valid";
}
@FWest98 Thanks for this! Are you using this with v6, and if so, with bootstrap? I'm curious which classes to use, they change all the time. This code is great, it's a pity they didn't accept your PR.