Search code examples
c#asp.net-mvcunobtrusive-validationfluentvalidation

How to create warning messages in FluentValidation


I would like to add style to the validation messages based on enum type. FluentValidation offers possibility to add custom state for messages by using WithState method. Depending on which enum is used it would add a class for that message in HTML, so later I could add styling to it.

Model validator class:

public class SampleModelValidator : AbstractValidator<SampleModelValidator>
{
    public SampleModelValidator()
    {
        RuleFor(o => o.Age)).NotEmpty()
                // Using custom state here
                .WithState(o => MsgTypeEnum.WARNING)
                .WithMessage("Warning: This field is optional, but better fill it!");
    }
}

Controller action method:

[HttpPost]
public ActionResult Submit(SampleModel model)
{
    ValidationResult results = this.validator.Validate(model);
    int warningCount = results.Errors
                .Where(o => o.CustomState?.ToString() == MsgTypeEnum.WARNING.ToString())
                .Count();
    ...
}

I noticed that ASP.NET MVC is using unobtrusive.js by default and adding class .field-validation-error to each error message. So I guess needs to override that logic somehow.

How can I add styles to validation messages depending on provided enum type?


Solution

  • I figured out how to implement this. First need to create a html helper that will build the html tags and add necessary classes to them. This helper allows to display multiple messages with different types for one field/property.

    Great article that explains how to do it, possibly the only one out there!
    http://www.pearson-and-steel.co.uk/

    Html Helper method

    public static MvcHtmlString ValidationMessageFluent<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, int? itemIndex = null)
    {
        List<ValidationFailure> validationFailures = html.ViewData["ValidationFailures"] as List<ValidationFailure>;
        string exprMemberName = ((MemberExpression)expression.Body).Member.Name;
        var priorityFailures = validationFailures.Where(f => f.PropertyName.EndsWith(exprMemberName));
    
        if (priorityFailures.Count() == 0)
        {
            return null;
        }
    
        // Property name in 'validationFailures' may also be stored like this 'SomeRecords[0].PropertyName'
        string propertyName = itemIndex.HasValue ? $"[{itemIndex}].{exprMemberName}" : exprMemberName;
    
        // There can be multiple messages for one property
        List<TagBuilder> tags = new List<TagBuilder>();
        foreach (var validationFailure in priorityFailures.ToList())
        {
            if (validationFailure.PropertyName.EndsWith(propertyName))
            {
                TagBuilder span = new TagBuilder("span");
                string customState = validationFailure.CustomState?.ToString();
    
                // Handling the message type and adding class attribute
                if (string.IsNullOrEmpty(customState))
                {
                    span.AddCssClass(string.Format("field-validation-error"));
                }
                else if (customState == MsgTypeEnum.WARNING.ToString())
                {
                    span.AddCssClass(string.Format("field-validation-warning"));
                }
    
                // Adds message itself to the html element
                span.SetInnerText(validationFailure.ErrorMessage);
                tags.Add(span);
            }
        }
    
        StringBuilder strB = new StringBuilder();
        // Join all html tags togeather
        foreach(var t in tags)
        {
            strB.Append(t.ToString());
        }
    
        return MvcHtmlString.Create(strB.ToString());
    }
    

    Also, inside controller action method need to get validator errors and store them inside a session.

    Controller action method

    // In controller you also need to set the ViewData. I don't really like to use itself
    // but did not found other solution
    [HttpPost]
    public ActionResult Submit(SampleModel model)
    {
       IList<ValidationFailure> errors = this.validator.Validate(model).Errors;
       ViewData["ValidationFailures"] = errors as List<ValidationFailure>;
    
        ...
    }
    

    And simply use that html helper inside view.

    View

    // In html view (using razor syntax)
    @for (int i = 0; i < Model.SomeRecords.Count(); i++)
    {
        @Html.ValidationMessageFluent(o => Model.SomeRecords[i].PropertyName, i)
    
        ...
    }
    

    The last index parameter is required only if looping through lists.