Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom formatting of validation summary and errors

The problem

Summary

For the validation summary you usually have something like this:

<div asp-validation-summary="ModelOnly" class="..."></div>

which, in case there are errors with an empty string as field/attribute will be displayed inside that div within a <ul> list.

What if I want to display them using a sequence of divs with a specific class attribute? Or any other custom formatting?

Field validation

For field validation you usually do:

<div class="form-group">
    <label asp-for="OldPassword"></label>
    <input asp-for="OldPassword" class="form-control" />
    <span asp-validation-for="OldPassword" class="text-danger"></span>
</div>

and the error gets inserted as text within the span element.

I'm using a template that requires the has-errors class to be applied to the form-group div element in case there are errors because it needs to style both the label and the input.

It also requires the span to be a div (for some unknown reasons) and surprisingly enough changing the span to div prevents the div from inserting the text of the error; not to mention that wrapping the span inside a div yields that problem that the div has proper spacing applied to it so even if there are no errors the div shows an annoying space.

Question (tl; dr)

What is the most idiomatic way of handling custom form validation formatting (trying to DRY, since my application has many forms) with custom rules like the ones shown above?

like image 925
Shoe Avatar asked Feb 04 '23 04:02

Shoe


1 Answers

Here are some extension points that you can consider to provide custom rendering for validation summary and field validation errors:

  • Customize existing validation tag helpers (Register new IHtmlGenerator)
  • Create new validation tag helpers (Register new Tag Helpers)

Customize existing validation tag helpers

asp-validation-summary and asp-validation-for tag helpers use GenerateValidationSummary and GenerateValidationMessage methods of the registered implementation of IHtmlGenerator service which is DefaultHtmlGenerator by default.

You can provide your custom implementation deriving DefaultHtmlGenerator and overriding those methods, then register the service at startup. This way those tag helpers will use your custom implementation.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddTransient<IHtmlGenerator, MyHtmlGenerator>();
}

Here is the link to source code of DefaultHtmlGenerator to help you to customize the implementation.

Example - Creating a new implementation IHtmlGenerator

Here is just a simple example to show required namespaces and methods and simply what can goes into your custom implementation. After you provided custom implementation, don't forget to register it in ConfigureServices like what I did above.

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Html;
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.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.Extensions.Options;
using System.Text.Encodings.Web;

namespace ValidationSampleWebApplication
{
    public class MyHtmlGenerator : DefaultHtmlGenerator
    {
        public MyHtmlGenerator(IAntiforgery antiforgery, IOptions<MvcViewOptions> optionsAccessor, IModelMetadataProvider metadataProvider, IUrlHelperFactory urlHelperFactory, HtmlEncoder htmlEncoder, ValidationHtmlAttributeProvider validationAttributeProvider) 
            : base(antiforgery, optionsAccessor, metadataProvider, urlHelperFactory, htmlEncoder, validationAttributeProvider)
        {
        }
        public override TagBuilder GenerateValidationMessage(ViewContext viewContext, ModelExplorer modelExplorer, string expression, string message, string tag, object htmlAttributes)
        {
            return base.GenerateValidationMessage(viewContext, modelExplorer, expression, message, tag, htmlAttributes);
        }
        public override TagBuilder GenerateValidationSummary(ViewContext viewContext, bool excludePropertyErrors, string message, string headerTag, object htmlAttributes)
        {
            return base.GenerateValidationSummary(viewContext, excludePropertyErrors, message, headerTag, htmlAttributes);
        }
    }
}

Create new validation tag helpers

You also can author your custom tag helpers. To do so, it's enough to derive from TagHelper and override Process methods.

Then you can simply register created tag helpers in the view or globally in _ViewImports.cshtml:

@using ValidationSampleWebApplication
@using ValidationSampleWebApplication.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, ValidationSampleWebApplication

Also when creating the custom tag helpers for validation you can consider:

  • Creating the validation tag helper from scratch
  • Drive from existing tag-helper classes

Example - Adding hasError class to a form-group div

In this example, I've created a asp-myvalidation-for which can be applied on div elements this way <div class="form-group" asp-myvalidation-for="LastName"> and will add hasError class to div if the specified field has validation error. Don't forget to register it in _ViewImports.cshtml like what I did above.

using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace ValidationSampleWebApplication
{
    [HtmlTargetElement("div", Attributes = MyValidationForAttributeName)]
    public class MyValidationTagHelper : TagHelper
    {
        private const string MyValidationForAttributeName = "asp-myvalidation-for";

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

        [HtmlAttributeName(MyValidationForAttributeName)]
        public ModelExpression For { get; set; }
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            base.Process(context, output);
            ModelStateEntry entry;
            ViewContext.ViewData.ModelState.TryGetValue(For.Name, out entry);
            if (entry != null && entry.Errors.Count > 0)
            {
                var builder = new TagBuilder("div");
                builder.AddCssClass("hasError");
                output.MergeAttributes(builder);   
            }
        }
    }
}

Example - Adding field-validation-error class to a form-group div

In the following example, I've added div support to the standard asp-validation-for tag helper. The existing tag helper just supports div element. Here I've added div support to the asp-validation-for tag helper and in case of error, it will add field-validation-error otherwise, in valid cases the div will have field-validation-valid class.

The default behavior of the tag is in a way that it doesn't make any change in content of the tag if the tag has contents. So for adding the tag helper to an empty span will add validation error to span, but for a div having some contents, it just changes the class of div. Don't forget to register it in _ViewImports.cshtml like what I did above.

using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.TagHelpers;

namespace ValidationSampleWebApplication
{
    [HtmlTargetElement("div", Attributes = ValidationForAttributeName)]
    public class MytValidationMessageTagHelper : ValidationMessageTagHelper
    {
        private const string ValidationForAttributeName = "asp-validation-for";
        public MytValidationMessageTagHelper(IHtmlGenerator generator) : base(generator)
        {
        }
    }
}
like image 57
Reza Aghaei Avatar answered Feb 06 '23 17:02

Reza Aghaei