8

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?

3 Answers 3

12

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)
        {
        }
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

So just to make sure I understand: the solution to the second problem (field validation) would be to add asp-validation-for to the form-group div and build the proper tag there copying label and input and adding classes to those too?
The answer generally shows you how you can customize out put of existing tag-helpers or register new tag helpers. I added some more details to the answer to make it more useful. About your specific requirement for applying some class to the div element, you need to create tag helper. I added two examples to the answer which you can use both of them.
0

Using a custom tag helper i came up with a following solution:

[HtmlTargetElement("ul", Attributes = AttributeName)]
public class ValidationSummaryLiItemsTagHelper : TagHelper
{
    private const string AttributeName = "model-validation-summary-list";

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

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        var errors = ViewContext.ModelState.Where(x => x.Key == "").SelectMany(x => x.Value.Errors).ToList();
        foreach (var error in errors)
        {
            output.Content.AppendFormat("<li>{0}</li>", error.ErrorMessage);
        }

        if (errors.Any() == false)
            output.SuppressOutput();
    }
}

Later i can use it like that:

<ul class="some-css-class" id="some-id-name" model-validation-summary-list></ul>

Then, for example, i can customize the ul container directly with bootstrap classes or add any html attribute.

Comments

0

This seems to be quite old and we are currently at .NET 7.0, but for me the solution by kzaw was not working as it is actually getting the error messages where the element name is empty -> Where(x => x.Key == ""). I have added a few lines in order to show in the element name in the summary. After that I'm creating an anonymous object that is holding the key (the element) and the error messages for this element - in case there are multiple validations failed for specific element/input field:

[HtmlTargetElement("ul", Attributes = AttributeName)]
public class CustomValidationSummaryTagHelper : TagHelper
{
    private const string AttributeName = "validation-summary-list";

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


    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        var errors = ViewContext
            .ModelState
            .Where(x => x.Key != "")
            .Select(x => new
            {
                Key = x.Key,
                Errors = x.Value.Errors.ToList()
            })
            .ToList();

        foreach (var error in errors)
        {
            var keyErrors = error.Errors;

            foreach (var keyError in keyErrors)
            {
                output.Content.AppendHtml($"<li>{error.Key} - {keyError.ErrorMessage}</li>");
            }


        }

        if (errors.Any() == false)
            output.SuppressOutput();
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.