unobtrusive client validation using fluentvalidation and asp.net mvc LessThanOrEqualTo not firing

30,894

Solution 1

Neither of the LessThanOrEqualTo or GreaterThanOrEqualTo rules are supported by client side validation as explained in the documentation.

This means that if you want to have client side validation for them you will need to write a custom FluentValidationPropertyValidator and implement the GetClientValidationRules method which will allow you to register a custom adapter and implement the client side validation logic for it in javascript.

If you are interested on how this could be achieved just ping me and I will provide an example.


Update

As request, I will try to show an example of how one could implement custom client side validation for the LessThanOrEqualTo rule. It is only a particular case with non-nullable dates. Writing such custom client side validator for all the possible case is of course possible but it will require significantly more efforts.

So we start with a view model and a corresponding validator:

[Validator(typeof(MyViewModelValidator))]
public class MyViewModel
{
    [Display(Name = "Start date")]
    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
    public DateTime StartDate { get; set; }

    public DateTime DateToCompareAgainst { get; set; }
}

public class MyViewModelValidator : AbstractValidator<MyViewModel>
{
    public MyViewModelValidator()
    {
        RuleFor(x => x.StartDate)
            .LessThanOrEqualTo(x => x.DateToCompareAgainst)
            .WithMessage("Invalid start date");
    }
}

Then a controller:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        var model = new MyViewModel
        {
            StartDate = DateTime.Now.AddDays(2),
            DateToCompareAgainst = DateTime.Now
        };
        return View(model);
    }

    [HttpPost]
    public ActionResult Index(MyViewModel model)
    {
        return View(model);
    }
}

and a view:

@model MyViewModel
@using (Html.BeginForm())
{
    @Html.Hidden("DateToCompareAgainst", Model.DateToCompareAgainst.ToString("yyyy-MM-dd"))

    @Html.LabelFor(x => x.StartDate)
    @Html.EditorFor(x => x.StartDate)
    @Html.ValidationMessageFor(x => x.StartDate)
    <button type="submit">OK</button>
}

All this is standard stuff so far. It will work but without client validation.

The first step is to write the FluentValidationPropertyValidator:

public class LessThanOrEqualToFluentValidationPropertyValidator : FluentValidationPropertyValidator
{
    public LessThanOrEqualToFluentValidationPropertyValidator(ModelMetadata metadata, ControllerContext controllerContext, PropertyRule rule, IPropertyValidator validator)
        : base(metadata, controllerContext, rule, validator)
    {
    }

    public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
    {
        if (!this.ShouldGenerateClientSideRules())
        {
            yield break;
        }

        var validator = Validator as LessThanOrEqualValidator;

        var errorMessage = new MessageFormatter()
            .AppendPropertyName(this.Rule.GetDisplayName())
            .BuildMessage(validator.ErrorMessageSource.GetString());

        var rule = new ModelClientValidationRule
        {
            ErrorMessage = errorMessage,
            ValidationType = "lessthanorequaldate"
        };
        rule.ValidationParameters["other"] = CompareAttribute.FormatPropertyForClientValidation(validator.MemberToCompare.Name);
        yield return rule;
    }
}

which will be registered in Application_Start when configuring our FluentValidation provider:

FluentValidationModelValidatorProvider.Configure(x =>
{
    x.Add(typeof(LessThanOrEqualValidator), (metadata, context, rule, validator) => new LessThanOrEqualToFluentValidationPropertyValidator(metadata, context, rule, validator));
});

And the last bit is the custom adapter on the client. So we add of course the 2 scripts to our page in order to enable unobtrusive client side validation:

<script src="@Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>

and the custom adapter:

(function ($) {
    $.validator.unobtrusive.adapters.add('lessthanorequaldate', ['other'], function (options) {
        var getModelPrefix = function (fieldName) {
            return fieldName.substr(0, fieldName.lastIndexOf(".") + 1);
        };

        var appendModelPrefix = function (value, prefix) {
            if (value.indexOf("*.") === 0) {
                value = value.replace("*.", prefix);
            }
            return value;
        }

        var prefix = getModelPrefix(options.element.name),
            other = options.params.other,
            fullOtherName = appendModelPrefix(other, prefix),
            element = $(options.form).find(":input[name=" + fullOtherName + "]")[0];

        options.rules['lessthanorequaldate'] = element;
        if (options.message != null) {
            options.messages['lessthanorequaldate'] = options.message;
        }
    });

    $.validator.addMethod('lessthanorequaldate', function (value, element, params) {
        var parseDate = function (date) {
            var m = date.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
            return m ? new Date(parseInt(m[1]), parseInt(m[2]) - 1, parseInt(m[3])) : null;
        };

        var date = parseDate(value);
        var dateToCompareAgainst = parseDate($(params).val());

        if (isNaN(date.getTime()) || isNaN(dateToCompareAgainst.getTime())) {
            return false;
        }

        return date <= dateToCompareAgainst;
    });

})(jQuery);

Solution 2

Darin's example has some obsolete stuff in it, so here is a more updated example that I have that does number comparisons. You can easily tweak it for date comparisons though:

Javascript:

(function ($)
{
    $.validator.addMethod("lessthanorequal", function(value, element, param)
    {
        return this.optional(element) || parseFloat(value) <= parseFloat(param);
    }, "Must be less than");

    $.validator.unobtrusive.adapters.add("lessthanorequal", ["field"], function (options)
    {
        options.rules["lessthanorequal"] = options.params.field;
        if (options.message) options.messages["lessthanorequal"] = options.message;
    });
})(jQuery);

C#

public class LessThanOrEqualPropertyValidator : FluentValidationPropertyValidator
{

    public LessThanOrEqualPropertyValidator(ModelMetadata metadata, ControllerContext controllerContext, PropertyRule rule, IPropertyValidator validator)
        : base(metadata, controllerContext, rule, validator)
    {
    }

    public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
    {
        if (!ShouldGenerateClientSideRules()) yield break;

        var formatter = new MessageFormatter().AppendPropertyName(Rule.PropertyName);
        string message = formatter.BuildMessage(Validator.ErrorMessageSource.GetString());
        var rule = new ModelClientValidationRule
        {
            ValidationType = "lessthanorequal",
            ErrorMessage = message
        };

         rule.ValidationParameters["field"] =  ((LessThanOrEqualValidator)Validator).ValueToCompare;
        yield return rule;
    }
}

Global.asax Application_Start:

FluentValidation.Mvc.FluentValidationModelValidatorProvider.Configure(x =>
{
    x.Add(typeof(LessThanOrEqualValidator), (metadata, context, description, validator) => new LessThanOrEqualPropertyValidator(metadata, context, description, validator));
});

So now any number rule that uses LessThanOrEqual will be validated client side.

Solution 3

LessThanOrEqualTo and GreaterThanOrEqualTo do not support clientside validation out of the box.

However, InclusiveBetween is supported. So you could use InclusiveBetween.

Example

RuleFor(x => x.StartDate)
    .InclusiveBetween(x.AbsoluteStartDate, x.AbsoluteEndDate)

See the documentation for mor information about supported clientside methods.

Share:
30,894

Related videos on Youtube

Admin
Author by

Admin

Updated on July 09, 2022

Comments

  • Admin
    Admin almost 2 years

    I have the following rules

    the 1st does work using unobtrusive, client side validation, the second does not

    any ideas why?

    RuleFor(x => x.StartDate)
        .LessThanOrEqualTo(x => x.EndDate.Value)
        .WithLocalizedMessage(() => CommonRes.Less_Than_Or_Equal_To, filters => CommonRes.Start_Date, filters => CommonRes.End_Date);
    
    RuleFor(x => x.StartDate)
        .GreaterThanOrEqualTo(x => x.AbsoluteStartDate)
        .LessThanOrEqualTo(x => x.AbsoluteEndDate)
        .WithLocalizedMessage(() => CommonRes.Between, filters => CommonRes.Start_Date, filters => filters.AbsoluteStartDate, filters => filters.AbsoluteEndDate);
    
    • Darin Dimitrov
      Darin Dimitrov about 12 years
      Are you sure that the first works? LessThanOrEqualTo is not one of the rules listed in the documentation as being supported by client validation. Which version of FV are you using?
    • Admin
      Admin about 12 years
      oh man!! it was 'LessThanOrEqualTo' is there any work around for this?
  • Darin Dimitrov
    Darin Dimitrov about 12 years
    @iwayneo, OK. The first question that I have is whether the property you are comparing against in the LessThanOrEqualTo validator has a corresponding field in the form i.e. are you comparing against a dynamic value (that can be changed by the user in an input field) or is it the value that this property had at the moment the view was rendered?
  • Admin
    Admin about 12 years
    the property being validated is a form field - the proerty we are comparing against is a static non-editable field known the moment the view was rendered - we store it in a hidden field in the form
  • Darin Dimitrov
    Darin Dimitrov about 12 years
    @iwayneo, you are storing the value to compare against in a hidden field??? Wait a minute. What would prevent an user from using for example FireBug to modify the value of this hidden to whatever date he wants and then entering whatever date he wants in the input field and thus short-circuiting even your server side validation??? That's a huge security threat for your application. How can you possibly compare against a value that comes from the client (hidden field in this case but easily modifiable)?
  • Admin
    Admin about 12 years
    i can swap it so we don't do it like that that is not an issue. this is an internal app used by internal accounts staff so not really worried about hackers to be honest. but your point is valid and i can skin the cat another way :) how would i move on with this if it were being retrieved another way?
  • Darin Dimitrov
    Darin Dimitrov about 12 years
    @iwayneo, please see my updated answer for an example that might give you some ideas. The example is far from complete, as taking into account all the cases would require significantly more efforts. Hopefully it will give you some hints.
  • Admin
    Admin about 12 years
    thank you! that is spot on - i have some show and tell meetings today but will report back as soon as this is implemented.
  • Gromer
    Gromer over 11 years
    In your Application_Start section of your example, you're registering typeof(LessThanOrEqualValidator). I'm a bit confused, as you haven't created anything of that type, so where did LessThanOrEqualValidator come from in this context? How does LessThanOrEqualToFluentValidationPropertyValidator play into this?
  • Darin Dimitrov
    Darin Dimitrov over 11 years
    @Gromer, if you look more carefully in my Application_Start method you will notice that I have registered the LessThanOrEqualToFluentValidationPropertyValidator validator with the LessThanOrEqualValidator rule.
  • Gromer
    Gromer over 11 years
    Ugh, didn't scroll to the right enough. My bad, thanks for the reply!!
  • Darin Dimitrov
    Darin Dimitrov over 10 years
    @PKKG, probably because you have some javascript error in your code.
  • Willy
    Willy over 9 years
    I followed your tutorial and when run the application, I've got error 'Object reference not set to an instance of an object' on validator.MemberToCompare.Name. I've checked that validator.MemberToCompare is null
  • Willy
    Willy over 9 years
    Client side validation run well, but the error message is not well-form 'PurchaseDate' must be less than or equal to '{ComparisonValue}'.
  • Willy
    Willy over 9 years
    There is also date format problem, I'm using dd/MM/yyyy pattern, for example current date is 20/10/2014. If I fill 15/10/2014, it will display 'PurchaseOrderDate' must be less than or equal to '{ComparisonValue}'.
  • JsonStatham
    JsonStatham over 8 years
    @DarinDimitrov do we have to create a new class that inherits from FluentValidationPropertyValidator every time we want to add a new custom clientside validator?
  • Ciaran Gallagher
    Ciaran Gallagher over 5 years
    Hi @DarinDimitrov, do you know if it would be possible to adapt this property validator for fluent validations that use the Must() method?
  • Jakub Keller
    Jakub Keller about 5 years
    This is way too much code to do something as trivial as validation.