44

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);
  • Are you sure that the first works? `LessThanOrEqualTo` is not one of the rules listed in the [documentation](http://fluentvalidation.codeplex.com/wikipage?title=mvc&referringTitle=Documentation) as being supported by client validation. Which version of FV are you using? – Darin Dimitrov Feb 21 '12 at 16:04
  • oh man!! it was 'LessThanOrEqualTo' is there any work around for this? –  Feb 21 '12 at 16:41

3 Answers3

84

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);
benmccallum
  • 1,241
  • 12
  • 27
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • @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? – Darin Dimitrov Feb 21 '12 at 17:14
  • 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 –  Feb 21 '12 at 17:19
  • @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)? – Darin Dimitrov Feb 21 '12 at 17:31
  • 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? –  Feb 21 '12 at 17:38
  • @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. – Darin Dimitrov Feb 22 '12 at 18:28
  • thank you! that is spot on - i have some show and tell meetings today but will report back as soon as this is implemented. –  Feb 23 '12 at 11:44
  • 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? – Gromer Dec 24 '12 at 17:12
  • 1
    @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. – Darin Dimitrov Dec 24 '12 at 23:25
  • Ugh, didn't scroll to the right enough. My bad, thanks for the reply!! – Gromer Dec 24 '12 at 23:28
  • @PKKG, probably because you have some javascript error in your code. – Darin Dimitrov Aug 14 '13 at 06:53
  • 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 Oct 20 '14 at 08:02
  • @DarinDimitrov do we have to create a new class that inherits from FluentValidationPropertyValidator every time we want to add a new custom clientside validator? – JsonStatham Dec 03 '15 at 11:49
  • Hi @DarinDimitrov, do you know if it would be possible to adapt this property validator for fluent validations that use the Must() method? – Ciaran Gallagher Oct 17 '18 at 08:53
  • This is way too much code to do something as trivial as validation. – Jakub Keller Apr 19 '19 at 14:57
6

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.

Gaff
  • 5,507
  • 3
  • 38
  • 49
  • Client side validation run well, but the error message is not well-form `'PurchaseDate' must be less than or equal to '{ComparisonValue}'.` – Willy Oct 20 '14 at 08:16
  • 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}'.` – Willy Oct 20 '14 at 08:20
5

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.

ndequeker
  • 7,932
  • 7
  • 61
  • 93