FluentValidation AddToModelState

HOORAY FOR THIS!

I was working on some admin functionality, and I added a validator to a model. The problem is the forms are contained within Telerik MVC Extensions tabs, so the validators for one tab are applied even if you are on a different tab. No dice! I really wanted to offer some nice error messages though, so I refused to quit! 🙂

I thought, “why can’t I just new up my validator manually”. I did this, and at first I wasn’t seeing what I needed. I eventually found that I had to get the result of validator.Validate(model) and then check result.IsValid. I was originally assuming I’d just be checking validator.IsValid. Anyway, here is what I had to that point:

var validator = 
    new Nop.Admin.Validators.Catalog
        .AddProductRelationshipAttributeValidator(_localizationService);
var result = validator.Validate(model);
if (result.IsValid == false) {
    // Do some error stuff here
}

The only thing now was how to get the result errors into the model state? I found a great thread on the FluentValidation discussion board that was asking the same thing – do I have to iterate the errors and manually add them to the ModelState? It turns out the answer is “yes”, but there is the handy extension method mentioned in the subject! TADA! So now, I have this nice bit of code:

var validator = 
    new Nop.Admin.Validators.Catalog
        .AddProductRelationshipAttributeValidator(_localizationService);
var result = validator.Validate(model);
if (result.IsValid == false) {
    Response.StatusCode = 400;
    result.AddToModelState(ModelState, "");
    return ModelState.JsonValidation(JsonRequestBehavior.AllowGet);
}

The ModelState.JsonValidation bit is another extension method I have to put the ModelState into a standardized JSON result that I then pass through a couple of js functions. Those functions make sure the validation passes, and if the validation does not pass it puts the errors in the validation summary manually. It is all pretty handy stuff.

If anyone has any better ideas, I am all ears!

FluentValidation NotEqual client-side validation

I wanted client-side validation for NotEqual. I started out writing my own and got 90% of the way there, then I got a little stuck. A quick Google revealed an existing Gist – however, the original implementation was only for NotEqual property comparison and I need a value compariosn. So, I forked it and made it work for my needs.

(function ($) {
    $.validator.addMethod("notequal", function (value, element, param) {
        if (param.indexOf("#") == -1) return value != param;
        return value != $(param).val();
    }, $.validator.messages.notequal);

    $.validator.unobtrusive.adapters.add("notequal", ["field"], 
    function (options) {
        options.rules["notequal"] = options.params.field;
        if (options.message) options.messages["notequal"] = options.message;
    });
})(jQuery);
FluentValidationModelValidatorProvider.Configure(provider =>
{
    provider.Add(typeof(NotEqualValidator), 
        (metadata, context, description, validator) => 
        new NotEqualClientRule(metadata, context, description, validator));
});
@model Test.Models.PersonModel
@using (Html.BeginForm())
{
    @Html.TextBoxFor(x => x.First)
    @Html.ValidationMessageFor(x => x.First)
    @Html.TextBoxFor(x => x.Last)
    @Html.ValidationMessageFor(x => x.Last)
    <button type="submit">OK</button>
}
[Validator(typeof(PersonValidator))]
public class PersonModel
{
    public string First { get; set; }
    public string Last { get; set; }
}
public class PersonValidator : AbstractValidator<PersonModel>
{
    public PersonValidator()
    {
        RuleFor(x => x.First).NotEqual(x => x.Last);
    }
}

Gist

When I look back at it, the whole hash check bit in the js just feels icky, so I think I’d go back and fix it up – probably just check to see if the jQuery selector exists, and if so compare .val() otherwise just compare the value. Whatever, it works for now even if it isn’t perfect…

More remote jQuery validation

In my previous post, I lament the troubles I had with remote validation. I got it working, but I am a stickler and a perfectionist, so I had to have more. I wanted unobtrusive validation and I wanted my error messages to show up properly. I also did not want to use the Remote annotation because I needed to use localization, which I have not been able to get working (it needs static strings).

I did some more banging my head on the keyboard, then I realized I could just emulate what was going on with the Remote annotation. So, I made a new FluentValidation rule and went about it. I realized soon I was missing someattributes, but when I tries to add attributes with dashes MVC balked. “WTF?” I thought. Thanks to the power of the googles, I am now no longer left wondering. I found this great post that gave me just what I needed. The gist is that when you define a rule, if you add additional validation parameters, they will be preceded by the original rule name. For example, I made a rule for data-remote, and I needed data-remote-url. If I added a ValidationParamter for “url”, it would append that to “data-remote-” and all would work as planned. HOORAY!

Remote jQuery validation

I just spent too much time banging me head against the wall trying yo get remote validation working. Each of the examples I found just did not seem to work, until I found this post from jquery4u.com. It used an interesting syntax that I had not seen anywhere. Until I found that post, all the examples used remote as an attribute under “rules”, but I could not seem to get it to work. The jquery4u syntax worked.

When I see multiple examples that I cannot get to work, I can never seem to let it go, so I started trying to get that syntax to work. I finally realized that, even though I had seen examples where data was passed, the way to get it to work was just to specify the URL and let jQuery validation do the rest. It creates a query param using the name you specify in the validator (so essentially your input name) as well as the value of the input. I finally got that working. This post from develoq.net helped set me straight.

My final struggle was to get unobtrusive validation working as well as an error message in my validation summary. I found how you can decorate an attribute on your model, but we use localization, and that just doesn;t work. We use FluentValidation, and that allows us to use our localization strings, so I wanted to see if FV could handle remote. It turns out it isn’t out of the box, but I found this post where a user created his own RemoteValidator.

It too me a while, but I knew it would and should all work!

Conditional validation

Related to my recent post about cascading drop down lists, I needed to do some conditional validation based on a drop down list selection. I found a great post on Simon Ince’s blog about conditional validation in MVC 3. He even has a new project to wrap up some of the conditional functionality that he blogged about, but I decided to roll my own since I would learn more going that route.

Using the project attached to his post, I started dissecting what he was doing. After a while, I got the client validation working, but I realized I was so focused on the client/server validation piece that I did not get my conditions correct! After a few tweaks, I finally got everything working. Here is the custom attribute:

public class ConditionalMaximumWeightAttribute : ValidationAttribute, 
                                                 IClientValidatable {

    private const string ERRORMSG = "Weight must not exceed {0} lbs.";

    public string DependentProperty { get; set; }
    public string DependentValue { get; set; }
    public int MaximumWeight { get; set; }

    public ConditionalMaximumWeightAttribute(string dependentProperty, 
                                             string dependentValue, 
                                             int maximumWeight) {
        this.DependentProperty = dependentProperty;
        this.DependentValue = dependentValue;
        this.MaximumWeight = maximumWeight;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
                         ModelMetadata metadata, ControllerContext context) {
        var rule = new ModelClientValidationRule() {
            ErrorMessage = String.Format(ERRORMSG, this.MaximumWeight),
            ValidationType = "maximumweight",
        };

        string depProp = BuildDependentPropertyId(metadata, context 
                                                            as ViewContext);

        rule.ValidationParameters.Add("dependentproperty", depProp);
        rule.ValidationParameters.Add("dependentvalue", this.DependentValue);
        rule.ValidationParameters.Add("weightvalue", this.MaximumWeight);

        yield return rule;
    }

    protected override ValidationResult IsValid(object value, 
                                        ValidationContext validationContext) {
        // get a reference to the property this validation depends upon
        var containerType = validationContext.ObjectInstance.GetType();
        var field = containerType.GetProperty(this.DependentProperty);

        if (field != null) {
            // get the value of the dependent property
            var dependentvalue = 
                field.GetValue(validationContext.ObjectInstance, null);

            var weight = 
                containerType.GetProperty(validationContext.DisplayName);
            int weightvalue = 
                (int)weight.GetValue(validationContext.ObjectInstance, null);

            // compare the value against the target value
            if (dependentvalue == this.DependentValue && 
                                  weightvalue > this.MaximumWeight) {
                // validation failed - return an error
                return new ValidationResult(String.Format(ERRORMSG, 
                                            this.MaximumWeight));
            }
        }

        return ValidationResult.Success;
    }

    private string BuildDependentPropertyId(ModelMetadata metadata, 
                                            ViewContext viewContext) {
        // build the ID of the property
        string depProp = viewContext.ViewData.TemplateInfo
                         .GetFullHtmlFieldId(this.DependentProperty);
        // unfortunately this will have the name of the current field appended 
        // to the beginning,
        // because the TemplateInfo's context has had this fieldname appended 
        // to it. Instead, we
        // want to get the context as though it was one level higher (i.e. 
        // outside the current property,
        // which is the containing object (our Person), and hence the same 
        // level as the dependent property.
        var thisField = metadata.PropertyName + "_";
        if (depProp.StartsWith(thisField))
            // strip it off again
            depProp = depProp.Substring(thisField.Length);
        return depProp;
    }
}

Gist

To put this in context, I need to restrict a maximum weight allowed for a carrier. At a certain point, we may as well go with another carrier because it is cheaper. However, I don’t want to restrict the maximum weight if the carrier that has been selected is the cheaper carrier.

The attribute is used like so, where PropertyName is the name of the related property to check, PropertyValue is the conditional value of the property, and MaximumWeight is an integer representing the maximum weight for the conditional:

public class ...

    [ConditionalMaximumWeight("PropertyName", "PropertyValue", MaximumWeight)]
    [Required(ErrorMessage = "Weight required")]
    public int? Weight { get; set; }

    ...
}

Here is the client-side implementation:

<script type="text/javascript">
    $.validator.addMethod("maximumweight",
        function (value, element, parameters) {
            var carrier = $("#" + parameters["dependentproperty"]).val();
            var carriervalue = parameters["dependentvalue"].toString();
            var weightvalue = Number(parameters["weightvalue"]);
            if (carrier == carriervalue && value > weightvalue) {
                return false;
            }
            return true;
        }
    );

    $.validator.unobtrusive.adapters.add(
    "maximumweight",
    ["weightvalue", "dependentproperty", "dependentvalue"],
    function (options) {
        options.rules["maximumweight"] = {
            weightvalue: options.params["weightvalue"],
            dependentproperty: options.params["dependentproperty"],
            dependentvalue: options.params["dependentvalue"]
        };
        options.messages["maximumweight"] = options.message;
    });
</script>

Gist

I should probably do a little null checking in on the client-side, but I may just find that out the hard way! 🙂