7

Question

The below code is working fine Server side and not Client side. Why ?


When I submit the form, control goes to BeAValidDate function to check the date is valid or not. Is there any way to Validate the date without going to server using Fluent Validation?

Scripts

<script src="jquery-1.7.1.min.js" type="text/javascript"></script>
<script src="jquery.validate.js" type="text/javascript"></script>
<script src="jquery.validate.unobtrusive.js" type="text/javascript"></script>

Model

public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        RuleFor(x => x.FromDate)
            .NotEmpty()
            .WithMessage("Date is required!")
            .Must(BeAValidDate)
            .WithMessage("Invalid Date");
    }

    private bool BeAValidDate(String value)
    {
        DateTime date;
        return DateTime.TryParse(value, out date);
    }
}

Controller

public class PersonController : Controller
{
    public ActionResult Index()
    {
       return View(new Person { FromDate = DateTime.Now.AddDays(2).ToString()});
    }

    [HttpPost]
    public ActionResult Index(Person p)
    {
        return View(p);
    }
}

View

@using (Html.BeginForm("Index", "Person", FormMethod.Post))
{   
    @Html.LabelFor(x => x.FromDate)
    @Html.EditorFor(x => x.FromDate)
    @Html.ValidationMessageFor(x => x.FromDate)

    <input type="submit" name="Submit" value="Submit" />
}
Imad Alazani
  • 6,688
  • 7
  • 36
  • 58
SMC
  • 237
  • 1
  • 5
  • 29
  • Do you have client side and unobtrusive validation activated in web.config? – JTMon Aug 09 '13 at 12:43
  • Did you set the FluentValidationModelValidatorProvider? – Fals Aug 09 '13 at 12:49
  • @JTMon : UnObstrusive with working properly with Data Annotations. I am curious to know about Client Side Fluent Validation for Date. – SMC Aug 10 '13 at 09:05
  • [This question](http://stackoverflow.com/a/9381502/138071) is similar to yours, here you can see how using `jquery.validate.js` to adapt it to `FluentValidation`. this is another question, [in this answer](http://stackoverflow.com/a/6888355/138071) which refers to this link [Integration with ASP.NET MVC](http://fluentvalidation.codeplex.com/wikipage?title=mvc&referringTitle=Documentation) where is explains how to integrate and extend some validations and to accept other. Here you can see the [NotEqual Fluent Validation validator with client side validation](https://gist.github.com/michaeljacob – andres descalzo Aug 14 '13 at 01:13

3 Answers3

6

Trick using Greater Then Or Equal To Validator. Works for me.

Global.asax - Application Start Event

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

Model

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

    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", 
                                                  ApplyFormatInEditMode = true)]
    public DateTime DateToCompareAgainst { get; set; }
}

Rule

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

FluentValidationPropertyValidator

public class GreaterThenOrEqualTo : FluentValidationPropertyValidator
{
    public GreaterThenOrEqualTo(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 GreaterThanOrEqualValidator;

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

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

Controller Action Method

public ActionResult Index()
{
    var model = new MyViewModel
    {
        StartDate = DateTime.Now.AddDays(2),
        DateToCompareAgainst = default(DateTime)  //Default Date
    };
    return View(model);
}
[HttpPost]
public ActionResult Index(Practise.Areas.FluentVal.Models.MyViewModel p)
{
    return View(p);
}

View

@using (Html.BeginForm("Index", "Person", FormMethod.Post, 
                                                new { id = "FormSubmit" }))
{   
    @Html.Hidden("DateToCompareAgainst", Model.DateToCompareAgainst);      
    @Html.LabelFor(x => x.StartDate)
    @Html.EditorFor(x => x.StartDate)
    @Html.ValidationMessageFor(x => x.StartDate)
    <button type="submit">
        OK</button>
}

Script

<script src="jquery-1.4.1.min.js" type="text/javascript"></script>
<script src="jquery.validate.js" type="text/javascript"></script>
<script src="jquery.validate.unobtrusive.js" type="text/javascript"></script>
<script type="text/javascript">
    (function ($) {
        $.validator.unobtrusive.adapters.add('greaterthanorequaldate', 
                                             ['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['greaterthanorequaldate'] = element;
            if (options.message != null) {
                options.messages['greaterthanorequaldate'] = options.message;
            }
        });

        $.validator.addMethod('greaterthanorequaldate', 
                               function (value, element, params) {
            var date = new Date(value);
            var dateToCompareAgainst = new Date($(params).val());

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

    })(jQuery);
</script>
Imad Alazani
  • 6,688
  • 7
  • 36
  • 58
0

There are things I don't understand about your settings and about what is not working. Do you mean the verification that the date must be valid is not working, or that the fact the date is required is not working?

Fluent validation is able to emit code for unobtrusive validation too, so the required constraint should work properly. The fact that the date is valid is completely another story. If you specify your FromDate as a DateTime (have you declared it as a DateTime or as a string?) the verification of correctness of date is AUTOMATICALLY performed by other validators included in the Mvc framework, so you dont need to repeat it in the fluent validation rules. However, before Mvc4 this validation check were performed ONLY on the server side. With Mvc 4 the Asp.net Mvc Team fixed this problem and extended the check also on the client side. However, on the client side, everything work propery only for the en-US date format, , since unobtrusive validation doesn't handle globalization. If you need date in other cultures you need to use the Globalize library, and you need to set up globalization on the client side. If you are interested to globalization you may see this post of my blog. To add automatic Date correction also to Mvc 3. You must define an extended ClientDataTypeModelValidatorProvider like the one of Mvc4. Below the code:

public class ClientDataTypeModelValidatorProviderExt : ClientDataTypeModelValidatorProvider
{
    public static Type ErrorMessageResources { get; set; }
    public static string NumericErrorKey { get; set; }
    public static string DateTimeErrorKey { get; set; }
    private static readonly HashSet<Type> _numericTypes = new HashSet<Type>(new Type[] {
        typeof(byte), typeof(sbyte),
        typeof(short), typeof(ushort),
        typeof(int), typeof(uint),
        typeof(long), typeof(ulong),
        typeof(float), typeof(double), typeof(decimal)
    });
    private static bool IsNumericType(Type type)
    {
        Type underlyingType = Nullable.GetUnderlyingType(type); // strip off the Nullable<>
        return _numericTypes.Contains(underlyingType ?? type);
    }
    internal sealed class NumericModelValidator : ModelValidator
    {
        public NumericModelValidator(ModelMetadata metadata, ControllerContext controllerContext)
            : base(metadata, controllerContext)
        {
        }

        public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
        {
            ModelClientValidationRule rule = new ModelClientValidationRule()
            {
                ValidationType = "number",
                ErrorMessage = MakeErrorString(Metadata.GetDisplayName())
            };

            return new ModelClientValidationRule[] { rule };
        }

        private static string MakeErrorString(string displayName)
        {
            // use CurrentCulture since this message is intended for the site visitor
            return String.Format(CultureInfo.CurrentCulture, ErrorMessageResources.GetProperty(NumericErrorKey, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static).GetValue(null, null) as string, displayName);
        }

        public override IEnumerable<ModelValidationResult> Validate(object container)
        {
            // this is not a server-side validator
            return Enumerable.Empty<ModelValidationResult>();
        }
    }
    internal sealed class DateTimeModelValidator : ModelValidator
    {
        public DateTimeModelValidator(ModelMetadata metadata, ControllerContext controllerContext)
            : base(metadata, controllerContext)
        {
        }

        public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
        {
            ModelClientValidationRule rule = new ModelClientValidationRule()
            {
                ValidationType = "globalizeddate",
                ErrorMessage = MakeErrorString(Metadata.GetDisplayName())
            };

            return new ModelClientValidationRule[] { rule };
        }

        private static string MakeErrorString(string displayName)
        {
            // use CurrentCulture since this message is intended for the site visitor
            return String.Format(CultureInfo.CurrentCulture, ErrorMessageResources.GetProperty(DateTimeErrorKey, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static).GetValue(null, null) as string, displayName);
        }

        public override IEnumerable<ModelValidationResult> Validate(object container)
        {
            // this is not a server-side validator
            return Enumerable.Empty<ModelValidationResult>();
        }
    }
    public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context)
    {

        if (metadata == null)
        {
            throw new ArgumentNullException("metadata");
        }
        if (context == null)
        {
            throw new ArgumentNullException("context");
        }
        List<ModelValidator> res = null;
        if (NumericErrorKey == null || ErrorMessageResources == null)
            res = base.GetValidators(metadata, context).ToList();
        else
        {
            res = new List<ModelValidator>();
            if (IsNumericType(metadata.ModelType))
            {
                res.Add(new NumericModelValidator(metadata, context));
            }
        }
        if ( (metadata.ModelType == typeof(DateTime) || metadata.ModelType == typeof(DateTime?)))
        {
            if(ErrorMessageResources != null && DateTimeErrorKey != null)
                res.Add(new DateTimeModelValidator(metadata, context));
        }
        return res;
    }
}

Then in the global.asax you must substititute the standard one with this:

var old = ModelValidatorProviders.Providers.Where(x => x is ClientDataTypeModelValidatorProvider).FirstOrDefault();
        if (old != null) ModelValidatorProviders.Providers.Remove(old);
        ModelValidatorProviders.Providers.Add(new ClientDataTypeModelValidatorProviderExt());

Now you have to add the javascript snipped that do the control on the client side:

$.validator.addMethod(
"globalizeddate",
 function (value, element, param) {
    if ((!value || !value.length) && this.optional(element)) return true; /*success*/       
    var convertedValue  = Globalize.parseDate(value);
            return !isNaN(convertedValue) && convertedValue;
  },
  "field must be a date/time"

);

There I used the Globalize function to verify correct date. Install it. It is the only way to have a date frormat compatible with the .net format. Moreover it works in all .net cultures. Using a standard javascript date parsing, is not compatible with the formats accepted by .Net in some browsers.

Francesco Abbruzzese
  • 4,139
  • 1
  • 17
  • 18
  • thanks for the answer. can you please share your experience for mvc3 ? – SMC Aug 12 '13 at 11:43
  • The way to add client side validation to fluent validation is to implement a validator, so you can't use simply .Must, since it doesn't give way to add validation. It is enough that your validator implements the IClientValidatable interface. The fluent validation project explain how to create a custom validator here: https://fluentvalidation.codeplex.com/wikipage?title=Custom&referringTitle=Documentation In order to add client side validation your custom validator must implement the IClientValidatable. – Francesco Abbruzzese Aug 12 '13 at 15:28
  • The way to impement IClientValidatable is exactly the same as with DataAnnotations. Unluckly for this reason it is not explained in the fluent validation web site. See here for a simple way to implement all client validation stuffs:http://stackoverflow.com/questions/5154231/how-does-dataannotations-really-work-in-mvc It is for DataAnnotations, bur as I said IT IS THE SAME. On the .Net side your validator must implement the same IClientValidatable class, and on the javascript side stuffs are exactly the same. – Francesco Abbruzzese Aug 12 '13 at 15:33
  • That said the check for data validity SHOULD NOT be implemented with a rule applied to each DateTime, but should be applied automatically to all DateTime as Mvc4 do. The way to do this is to substitute the ClientDataTypeModelValidatorProvider of Mvc 3 that signals just ill formatted numbers, with one that validates also dates (like the one of Mvc 4). I have the code that solve the problem, but is not so self contained to be inserted here, if you are interested I can provide you that code. – Francesco Abbruzzese Aug 12 '13 at 15:42
  • Sample code of what? A ClientDataTypeModelValidatorProvider that is able to signall automatically all hill formatted dates that is what you need in your example, or of how to Implement the IClientValidatable interfaces + associated javascript stuff? – Francesco Abbruzzese Aug 13 '13 at 17:31
  • Yes. Need to correct my code so that it can start to work on client side to check valid date. – SMC Aug 14 '13 at 04:47
-1

In MVC 3 below code should work fine.

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

@using (Html.BeginForm("Index", "Person", FormMethod.Post))
{   
    @Html.LabelFor(x => x.FromDate)
    @Html.EditorFor(x => x.FromDate)
    @Html.ValidationMessageFor(x => x.FromDate)

    <input type="submit" name="Submit" value="Submit" />
}

Simple working Example in MVC 4

_Layout.cshtml:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>@ViewBag.Title - My ASP.NET MVC Application</title>
        <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
        <meta name="viewport" content="width=device-width" />


        @Styles.Render("~/Content/css")
        @Scripts.Render("~/bundles/modernizr")
    </head>
    <body>

        <div id="body">
            @RenderSection("featured", required: false)
            <section class="content-wrapper main-content clear-fix">
                @RenderBody()
            </section>
        </div>

        @Scripts.Render("~/bundles/jquery")
        @RenderSection("scripts", required: false)


    </body>
</html>

View:

@model Mvc4Test.Models.Person

@{
    ViewBag.Title = "test";

}

<h2>test</h2>

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>Part</legend>

        <div class="editor-label">
            @Html.LabelFor(model => model.Name)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Name)
            @Html.ValidationMessageFor(model => model.Name)
        </div>



        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
}

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

For more details.

Imad Alazani
  • 6,688
  • 7
  • 36
  • 58
chamara
  • 12,649
  • 32
  • 134
  • 210