PortableSheep / Pliant

A jQuery validation plugin that allows easy extending/overriding of rules, as well as defining field validation by html comments.
http://portablesheep.github.com/Pliant/
Other
6 stars 3 forks source link

how to do server side validation? #18

Closed fc closed 9 years ago

fc commented 9 years ago

"Modified the rule processing during field add to allow adding of rules without a validate function, as long as they have a message. This allows the user do things such as pass the fields/rules back to a server, do some server validation, and still be able to mark it as invalid on the client side on post back."

So... how do I actually do this? I've looked at your examples and the documentation but it's unclear how to do this.

I get that I set the message property on a rule but then how/where do I call the ajax method to do this and then how can I invalidate the field?

PortableSheep commented 9 years ago

There's no built in server side validation with Pliant itself. What I was referring to was the ability to make Pliant dump the rules/field states to a hidden input for example, and then you could do custom validation. The server side could then set another hidden input with JSON as to the new field state so Pliant knows how to mark the fields.

I can't go into enough detail right this moment to provide examples, but I will pull the code I use in production and use that as an example here as soon as possible. Should be this week. I'll update the example wiki as well.

fc commented 9 years ago

here is the pliant rule:

                    ,emailexisting: { 
                        message: translations.email_address_exists,
                        validate: function(o) {
                            return isValidEmail===true;
                        }

                    }

I ended up doing something like this:

    $('#email').change(function() {
        // let pliant handle other email validation rules -- this should be different but you get the idea and use pliant to check:
        if ($(this).val()=='' || !isValidEmailCheck($(this).val())) {
            return false;
        }

        $.post('/some-url/is-unique-email/', {email:$(this).val()}, function(response){
            isValidEmail = response.isValid;
            formSettings.plInst.validateField( $('#email') ); // revalidate field
        });
    });

Not perfect but functional for my needs for now...

PortableSheep commented 9 years ago

I use ajax in a similar way, but as a shared rule.

webservice: {
    message: 'Invalid data.',
    url: null,
    getData: null,
    allowemptysource: false,
    expectedReturnValue: true,
    validate: function (o) {
        if (o.allowemptysource && this.val() == '') {
            return true;
        }
        var valid = false;
        if (o.url && o.getData) {
            var inData = o.getData.call(this);
            if (inData instanceof Object) {
                inData = JSON.stringify(inData);
            }
            $.ajax({
                url: o.url,
                async: false,
                data: inData,
                timeout: 20000,
                contentType: 'application/json; charset=utf-8',
                dataType: 'json',
                type: 'POST',
                success: function (msg) {
                    var isSuccessful = false;

                    for(var i in msg) {
                        if($.isPlainObject(msg[i])) {
                            for(var y in msg[i]) {
                                if(y == 'IsSuccessful') {                       
                                    isSuccessful = msg[i][y];
                                }
                            }
                        } else {
                            isSuccessful = msg[i]
                        }
                    }

                    valid = o.expectedReturnValue == isSuccessful;
                },
                error: function () {
                    valid = false;
                }
            });
        }
        return valid;
    }
}

Usage in a fields rule collection:

webservice: {
    allowemptysource: true,
    url: '~/WebServices/Role.svc/VerifyUniqueRoleName")',
    getData: function () {
        return { name: roleName.val() };
    },
    message: 'This role name already exists.'
}

That's how I use ajax to validate again server side stuff on my C#/ASP.NET projects. That's not what I was referring to in my commit message though. There's no reason you can't stick with ajax calls to web services for server side validation in that sense. I just had a requirement at my day job to allow all validation to be "double checked" on the server without multiple ajax calls.

I'm formatting an example of how I use it via C#/ASP.NET which I'll post in a moment. The catch is that it's a custom implementation because the expectation would be to get the rules and field state from the client, deserialize them on the server, validate the field values against server rules, set the fields valid state, and serialize the field states back to the client on POST back where I would then mark them as invalid via JS at that point... which is where the feature mentioned in the commit actually comes into play.

PortableSheep commented 9 years ago

On the "Master.Page" I've added some ASP.NET hidden inputs to store the invalid fields JSON and the validation state JSON. If you're doing this on say PHP, then this would just be on your index or something as normal hidden inputs.

<asp:HiddenField ID="hidInvalidFields" runat="server" ClientIDMode="Static" EnableViewState="False" />
<asp:HiddenField ID="hidFieldValidationState" runat="server" ClientIDMode="Static" EnableViewState="False" />

Because I didn't want my custom "server side" pass through for validation to go into Pliants core code due to it being a custom implementation that leverages some core functions... I instead wrote this wrapper around Pliant to add the options so they could be used site wide without touching the core code:

if (jQuery.fn.pliant) {
    var origPliant = jQuery.fn.pliant;
    jQuery.fn.pliant = function(o) {
        var opt = $.extend(true, {
            plugins: {
                utils: {}
            },
            //Some custom options that I'll use later...
            disableClientValidation: false, //Override this to disable client side validation when testing server side validation.
            enableServerSideValidation: true, //Override this to disable server side validation. When enabled, the hidden fields are used.
            invalidStateHiddenField: $('#hidInvalidFields'), //Hidden field on page where this code behind writes the fields/rules that are invalid. Override this for custom hidden field per instance if needed.
            fieldValidationStateField: $('#hidFieldValidationState') //Hidden field on page where MV writes all the current rules/properties for fields. 
        }, o), pl = origPliant.apply(this, [opt]), oldValidate = pl.Validate;

        //Hi-jack the validate function so we can add the disable client logic but still allow server side testing.
        pl.Validate = function() {
            if (!opt.disableClientValidation) {
                return oldValidate.call(pl);
            }
            opt.fieldValidationStateField.val(JSON.stringify(pl.GetFieldRules()));
            return true;
        };

        //Because this code is re-parsed/executed on load... we can check the invalidStateHiddenField for data and parse it to set any invalid states returned by the server during a post.
        if (opt.invalidStateHiddenField) {
            if (opt.invalidStateHiddenField.val()) {
                //Deserialize the invalid fields set from the code behind, loop them, and build up a list of invalid rules for the field.
                var invalidStates = JSON.parse(opt.invalidStateHiddenField.val());
                for (var i in invalidStates) {
                    var field = invalidStates[i], invalidRules = {};
                    for (var r in field.rules) {
                        invalidRules[field.rules[r].name] = false;
                    }

                    //Set the state of the field.
                    pl.SetState({ field: $('#' + field.id), rules: invalidRules });
                }
            }

            //Subscribe to the form validate event, and set the hidden state field with the PL defined fields/rules/properties for server side validation.
            pl.Subscribe('onFormValidate', function() {
                opt.fieldValidationStateField.val(JSON.stringify(this.GetFieldRules()));
            });
        }

        return pl;
    };
}

So on the server side now, I created some classes in C# to handle deserialization and validation:

    [Serializable]
    public class RuleProperty
    {
        public string key { get; set; }
        public string value { get; set; }
    }

    [Serializable]
    public class RuleObject
    {
        public string name { get; set; }
        public List<RuleProperty> properties { get; set; }
    }

    [Serializable]
    public class FieldObject
    {
        public string id { get; set; }
        public List<RuleObject> rules { get; set; }
    }

    public class Pliant
    {
        public Page PageControl
        {
            get
            {
                if (HttpContext.Current.Handler != null)
                {
                    return HttpContext.Current.Handler as Page;
                }
                return null;
            }
        }

        private HiddenField HiddenStateField { get; set; }
        private HiddenField HiddenInvalidField { get; set; }

        public Pliant(string hiddenStateField, string hiddenInvalidField)
        {
            HiddenStateField = FindControl.FindControlRecursive(PageControl.Master, hiddenStateField) as HiddenField;
            HiddenInvalidField = FindControl.FindControlRecursive(PageControl.Master, hiddenInvalidField) as HiddenField;
        }

        public Pliant() : this("hidFieldValidationState", "hidInvalidFields")
        {
        }

        private List<FieldObject> _fields;
        public List<FieldObject> Fields
        {
            get { return _fields ?? (_fields = JsonConvert.DeserializeObject<List<FieldObject>>(HiddenStateField.Value)); }
        }

        private readonly List<KeyValuePair<string, Rule>> _additionalRules = new List<KeyValuePair<string, Rule>>(); 
        public void AddRule(string name, Rule rule)
        {
            var index = _additionalRules.FindIndex(p => p.Key == name);
            if (index > -1)
            {
                throw new Exception(String.Format("Rule {0} already exists.", name));
            } 
            _additionalRules.Add(new KeyValuePair<string, Rule>(name, rule));
        }

        public bool IsValid()
        {
            if (!string.IsNullOrEmpty(HiddenStateField.Value))
            {
                var isValid = true;
                var invalidFields = new List<FieldObject>();
                foreach (var valObj in Fields)
                {
                    var invalidRules = new List<RuleObject>();
                    foreach (var rule in valObj.rules)
                    {
                        var valIndex = _additionalRules.FindIndex(p => p.Key.ToLower() == rule.name.ToLower());
                        var val = valIndex > -1 ? _additionalRules[valIndex].Value : ValidationRules.GetValidator(rule.name, rule.properties);
                        if (val != null)
                        {
                            val.ControlID = valObj.id;
                            var rValid = val.IsValid();
                            if (!rValid)
                            {
                                invalidRules.Add(rule);
                            }
                            isValid &= rValid;
                        }
                    }
                    if (invalidRules.Count > 0)
                    {
                        invalidFields.Add(new FieldObject {id = valObj.id, rules = invalidRules});
                    }
                }

                return isValid;
            }
            return true;
        }
    }

I then created a base class for my Rules to inherit from:

    public abstract class Rule
    {
        public Page PageControl 
        { 
            get { return HttpContext.Current.Handler != null ? HttpContext.Current.Handler as Page : null; }
        }

        public string ControlValue()
        {
            return ControlValue(ControlID);
        }

        public string ControlValue(string controlId)
        {
            var control = FindControl.FindControlRecursive(PageControl, controlId);
            if (control != null)
            {
                var controlType = control.GetType();
                if (controlType == typeof(TextBox))
                {
                    return ((TextBox) control).Text;
                }
                if (controlType == typeof(HtmlInputFile))
                {
                    return ((HtmlInputFile)control).PostedFile.FileName;
                }
                if (controlType == typeof(DropDownList))
                {
                    return ((DropDownList) control).SelectedValue;
                }
            }
            return null;
        }

        public string ControlID { get; set; }
        public List<RuleProperty> Properties { get; set; }
        public virtual bool IsValid()
        {
            return true;
        }
    }

Followed by creating the rules themeselves (included just two rules for the example):

    public class ValidationRules
    {
        public static Rule GetValidator(string name, List<RuleProperty> properties)
        {
            Rule valRule = null;
            switch (name.ToLower())
            {
                case "required":
                    valRule = new Required();
                    break;
                case "length":
                    valRule = new Length();
                    break;
            }
            if (valRule != null)
            {
                if (properties != null)
                {
                    valRule.Properties = properties;
                }
            }
            return valRule;
        }

        public class Required : Rule
        {
            public override bool IsValid()
            {
                var ignoreServerValidationProperty = Properties.Find(p => p.key == "IgnoreServerValidation");
                bool ignoreValidation = false;
                if (ignoreServerValidationProperty != null && bool.TryParse(ignoreServerValidationProperty.value, out ignoreValidation) && ignoreValidation)
                {
                    return true;
                }
                var val = ControlValue();
                if (val == null)
                {
                    return true;
                }
                return (val.Length > 0);
            }
        }

        public class Length : Rule
        {
            public override bool IsValid()
            {
                var isValid = true;
                var min = Properties.Find(p => p.key == "min");
                var max = Properties.Find(p => p.key == "max");
                var value = ControlValue();
                if (min != null && !string.IsNullOrEmpty(min.value))
                {
                    int minVal;
                    Int32.TryParse(min.value, out minVal);
                    isValid &= value.Length >= minVal;
                }
                if (min != null && !string.IsNullOrEmpty(max.value) && max.value != "0")
                {
                    int maxVal;
                    Int32.TryParse(max.value, out maxVal);
                    isValid &= value.Length <= maxVal;
                }
                return isValid;
            }
        }
    }

Then in the code behind on say a c# button click handler that causes a POST back, I would simply call:

    var plInst = new Pliant();
    if (plInst.IsValid()) {
        //Do some logic to complete the flow.
        return;
    }
    //Otherwise do nothing because we're not valid... at this point once the page renders, the client side will decorate the fields based on the server sides data it passed to the hidden field.

Mind you this was never meant to be full server side validation in the sense of it being a fall back if JS is turned off or something, since it relies heavily on JS to work. I'm using it more as a sanity check to ensure no one circumvented certain client side rules when going through a critical work flow.

I think that is everything related to how I've implemented my own server side sanity checks... if I come across anything else I'll be sure to mention it.

PortableSheep commented 9 years ago

Feel free to re-open if/when you want to discuss this more.