Unobtrusive validation C# MVC Razor - c#

Is it possible to have unobtrusive validation to make a field required but only if other properties change?
For Example
[Required]
public Decimal Income {get; set;}
[Required]
public Decimal Tax {get; set;}
//Required if tax or income changes
public string ChangeReason {get; set;}
I thought about having multiple backing store fields and writing a Custom Validator to compare these, but wondered if anyone had a better suggestion?

Custom Validator is the way to go. I had to build something similar a while back.
I'd set up a hidden value - "Changed" - set it to true whenever the user mods the fields of interest.
Set a RequiredIf validator on the 2 properties of interest:
[RequiredIf("Changed", true, ErrorMessage = "Required")]
The code for a RequiredIf validator is shown below:
public class RequiredIfAttribute : ValidationAttribute, IClientValidatable
{
private RequiredAttribute _innerAttribute = new RequiredAttribute();
public string DependentProperty { get; set; }
public object TargetValue { get; set; }
public RequiredIfAttribute(string dependentProperty, object targetValue)
{
this.DependentProperty = dependentProperty;
this.TargetValue = targetValue;
}
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);
// compare the value against the target value
if ((dependentvalue == null && this.TargetValue == null) ||
(dependentvalue != null && dependentvalue.Equals(this.TargetValue)))
{
// match => means we should try validating this field
if (!_innerAttribute.IsValid(value))
// validation failed - return an error
return new ValidationResult(this.ErrorMessage, new[] { validationContext.MemberName });
}
}
return null;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule()
{
ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
ValidationType = "requiredif",
};
string depProp = BuildDependentPropertyId(metadata, context as ViewContext);
// find the value on the control we depend on;
// if it's a bool, format it javascript style
// (the default is True or False!)
string targetValue = (this.TargetValue ?? "").ToString();
if (this.TargetValue.GetType() == typeof(bool))
targetValue = targetValue.ToLower();
rule.ValidationParameters.Add("dependentproperty", depProp);
rule.ValidationParameters.Add("targetvalue", targetValue);
yield return rule;
}
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;
}
}
Javascript:
/// <reference path="jquery-1.4.4-vsdoc.js" />
/// <reference path="jquery.validate.unobtrusive.js" />
$.validator.addMethod('requiredif',
function (value, element, parameters) {
var id = '#' + parameters['dependentproperty'];
// get the target value (as a string,
// as that's what actual value will be)
var targetvalue = parameters['targetvalue'];
targetvalue =
(targetvalue == null ? '' : targetvalue).toString();
// get the actual value of the target control
// note - this probably needs to cater for more
// control types, e.g. radios
var control = $(id);
var controltype = control.attr('type');
var actualvalue =
controltype === 'checkbox' ?
control.attr('checked').toString() :
control.val();
// if the condition is true, reuse the existing
// required field validator functionality
if (targetvalue === actualvalue)
return $.validator.methods.required.call(
this, value, element, parameters);
return true;
}
);
$.validator.unobtrusive.adapters.add(
'requiredif',
['dependentproperty', 'targetvalue'],
function (options) {
options.rules['requiredif'] = {
dependentproperty: options.params['dependentproperty'],
targetvalue: options.params['targetvalue']
};
options.messages['requiredif'] = options.message;
});

It is possible. You can write your own attribute, to do this exactly.
It basically requires two steps:
Write your own attribute, make it inherit ValidationAttribute amd implement IClientValidatable
Write a Jquery validation adapter to support it
A good working sample is described in this post.
I used a similar approach to create a dependency validation (one field could have values only if another was filled)

Related

Web Api validating optional Url with DataAnnotations

Is there a way to make the [Url] validation optional?
As an example
public class Company
{
[Required, StringLength(63, MinimumLength = 2)]
public string Name { get; set; }
[Url, StringLength(127)]
public string Url { get; set; }
}
The above validation works exactly as i intended it to with the exception that the Url property cannot be optional. I don't want the validation to throw an error if it is blank rather I just want it to validate only when a user enters a value.
UPDATE:
Taking into consideration the answer below from Hamid Shahid. I just added a condition to work around my issue until something better comes up:
//Making url validation optional, fix
if (company.Url == string.Empty) company.Url = null;
The URLAttribute validation returns true for null value. If the URL property has a value of NULL, it would be considered as a valid value. If you are setting it to an empty string, it would be considered invalid.
The code below show decompiled version of UrlAttribute class
// System.ComponentModel.DataAnnotations.UrlAttribute
[__DynamicallyInvokable]
public override bool IsValid(object value)
{
if (value == null)
{
return true;
}
string text = value as string;
if (UrlAttribute._regex != null)
{
return text != null && UrlAttribute._regex.Match(text).Length > 0;
}
return text != null && (text.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) || text.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase) || text.StartsWith("ftp://", StringComparison.InvariantCultureIgnoreCase));
}

WPF validation by attributes check if property is valid

I have a ViewModel in my C# WPF application which contains several properties like this one
public class ExecutionsCreateViewModel : ValidationViewModelBase
{
[Required(ErrorMessage = "Execution name is required.")]
[StringLength(60, ErrorMessage = "Execution name is too long.")]
public string ExecutionName { get; set; }
[...]
}
Thats my ValidationViewModelBase class
public abstract class ValidationViewModelBase : IDataErrorInfo
{
string IDataErrorInfo.Error
{
get
{
throw new NotSupportedException("IDataErrorInfo.Error is not supported.");
}
}
string IDataErrorInfo.this[string propertyName]
{
get
{
if (string.IsNullOrEmpty(propertyName))
{
throw new ArgumentException("Invalid property name", propertyName);
}
string error = string.Empty;
var value = GetValue(propertyName);
var results = new List<ValidationResult>(1);
var result = Validator.TryValidateProperty(
value,
new ValidationContext(this, null, null)
{
MemberName = propertyName
},
results);
if (!result)
{
var validationResult = results.First();
error = validationResult.ErrorMessage;
}
return error;
}
}
private object GetValue(string propertyName)
{
PropertyInfo propInfo = GetType().GetProperty(propertyName);
return propInfo.GetValue(this);
}
}
And this is my TextBox in XAML
<TextBox Text="{Binding ExecutionName, UpdateSourceTrigger=PropertyChanged, NotifyOnValidationError=True, ValidatesOnExceptions=True, ValidatesOnDataErrors=True}"/>
Attributes are working, UI is correctly notified when property becomes invalid ("Invalid" VisualState is triggered).
The problem is, I don't know how to check in Create method if certain property is currently valid or not.
private void Create()
{
if(/*check if property is invalid*/)
{
MessageBox.Show(/*property ErrorMessage*/);
return;
}
//Do something with valid properties
}}
I've tried with Validator.ValidateProperty (1, 2, 3) but it's not working and/or it's too messy. I was also doing tricks like
try
{
ExecutionName = ExecutionName;
}
catch(Exception ex)
{
MessageBox.Show(ex.Message);
return;
}
But it's not working in some scenarios and does not look very professional.
Maybe ValidateProperty is the key, but after many "tutorials" I still doesn't know how to fill it to my needs.
Also there is second small thing. Attributes always validate its properties, so when user receive fresh form the ExecutionName will always be null thus Required attribute will mark it as invalid, so that control will automatically turn red. Is there a way to skip validation at initialization?
The problem is, I don't know how to check in Create method if certain property is currently valid or not.
The same way as you do in your ValidationViewModelBase class, e.g.:
private void Create()
{
var value = this.ExecutionName; //the property to validate
var results = new List<ValidationResult>();
var result = Validator.TryValidateProperty(
value,
new ValidationContext(this, null, null)
{
MemberName = "ExecutionName" //the name of the property to validate
},
results);
if (!result)
{
var validationResult = results.First();
MessageBox.Show(validationResult.ErrorMessage);
}
//...
}

CompareAttribute for WPF?

Does anyone know of a CompareAttribute data annotation for WPF, or a way of achieving the same result in WPF?
For those that don't immediately know, CompareAttribute is a property data annotation for validating in WPF, it takes a string for a second property and returns true if the decorated property and the passed property match.
Basically I need to validate a password change form, to ensure the "retyped password" matches the new password, and do this with data annotations so that i can use the xaml validation template.
You can create your own custom validation logic by creating your own CustomValidationAttribute descrided here.
Try Custom Validator like this
public class EqualsValidationAttribute : ValidationAttribute
{
string propertyToCompare;
public EqualsValidationAttribute(string propertyToCompare)
{
this.propertyToCompare = propertyToCompare;
}
public EqualsValidationAttribute(string propertyToCompare,string errorMessage):this(propertyToCompare)
{
this.ErrorMessage = propertyToCompare;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var propInfo=validationContext.ObjectInstance.GetType().GetProperty(propertyToCompare);
if (propInfo != null)
{
var propValue=propInfo.GetValue(validationContext.ObjectInstance);
if(value!=null && propValue!=null && !string.IsNullOrEmpty(value.ToString()) && !string.IsNullOrEmpty(propValue.ToString()) //if either one is empty dont Validate
&& (value.ToString() != propValue.ToString()))
return new ValidationResult(ErrorMessage);
}
else
throw new NullReferenceException("propertyToCompare must be the name of property to compare");
return ValidationResult.Success;
}
}
and use it in Entity like this
[Required(ErrorMessage="Password Required")]
public string Password {
get { return password; }
set { password = value; RaisePropertyChanged("Password"); }
}
[EqualsValidationAttribute("Password", ErrorMessage = "Confirm password must be same as password")]
public string ConfirmPassword {
get { return confirmedpassword; }
set { confirmedpassword = value; RaisePropertyChanged("ConfirmPassword"); }
}
}

Custom Validation Attribute Multiple Times on same field

How can I use Same Custom Validation Attribute Multiple Times on Same Field or simply enable AllowMultiple=true, for both server side and client side validation??
I have a following Custom Validation Attribute:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property,
AllowMultiple = true, Inherited = true)]
public class RequiredIfAttribute : ValidationAttribute,IClientValidatable
{
public RequiredIfAttribute(string dependentProperties,
string dependentValues = "",
string requiredValue = "val")
{
}
}
Where in dependentProperties I can specify multiple dependant properties seperated by comma, in dependentValues I can specify for which values of dependant properties validation should process and finally in requiredValue I can specify expected value for the field to be validated.
In my model there are two properties LandMark, PinCode and I want to use validation as follows:
public string LandMark { get; set; }
[RequiredIf("LandMark","XYZ","500500")]
[RequiredIf("LandMark", "ABC", "500505")]
public string PinCode { get; set; }
The values here are just for example, as per it seems I can add the attribute multiple times and don't get any compile error, I have implemented TypeID in attribute and it works well from serverside if I remove client validation from it. But when I am implementing IClientValidatable on the attribute, it gives me an error:
"Validation type names in unobtrusive client validation rules must be unique."
Any help how can I solve it??
The Problem
Validation Attributes have two environments they can validate against:
Server
Client
Server Validation - Multiple Attributes Easy
If you have any attribute with:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class RequiredIfAttribute : ValidationAttribute
And have put it on your class property like this:
public class Client
{
public short ResidesWithCd { get; set; };
[RequiredIf(nameof(ResidesWithCd), new[] { 99 }, "Resides with other is required.")]
public string ResidesWithOther { get; set; }
}
Then anytime the Server goes to validate an object (ex. ModelState.IsValid), it will check every ValidationAttribute on each property and call .IsValid() to determine validity. This will work fine, even if AttributeUsage.AllowMultiple is set to true.
Client Validation - HTML Attribute Bottleneck
If you enable client side by implementing IClientValidatable like this:
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var modelClientValidationRule = new ModelClientValidationRule
{
ValidationType = "requiredif",
ErrorMessage = ErrorMessageString
};
modelClientValidationRule.ValidationParameters.Add("target", prop.PropName);
modelClientValidationRule.ValidationParameters.Add("values", prop.CompValues);
return new List<ModelClientValidationRule> { modelClientValidationRule };
}
Then ASP.NET will emit the following HTML when generated:
(As long as ClientValidationEnabled &
UnobtrusiveJavaScriptEnabled are enabled)
<input class="form-control" type="text" value=""
id="Client_CommunicationModificationDescription"
name="Client.CommunicationModificationDescription"
data-val="true"
data-val-requiredif="Communication Modification Description is required."
data-val-requiredif-target="CommunicationModificationCd"
data-val-requiredif-values="99" >
Data Attributes are the only vehicle we have for dumping rules into the client side validation engine which will search for any attributes on the page via a built in or custom adapter. And once part of the set of client side rules, it'll be able to determine the validity of each parsed rule with a built in or custom method.
So we can call jQuery Validate Unobtrusive to look for and parse these attributes by adding a custom adapter which will add a validation rule to the engine:
// hook up to client side validation
$.validator.unobtrusive.adapters.add('requiredif', ['target', 'values'], function (options) {
options.rules["requiredif"] = {
id: '#' + options.params.target,
values: JSON.parse(options.params.values)
};
options.messages['requiredif'] = options.message;
});
We can then tell that rule how function and determine validity by adding a custom method like this which will add a custom way to evaluate requiredif rules (as opposed to date rules or regex rules) which will rely on the parameters we loaded earlier through the adapter:
// test validity
$.validator.addMethod('requiredif', function (value, element, params) {
var targetHasCondValue = targetElHasValue(params.id, params.value);
var requiredAndNoValue = targetHasCondValue && !value; // true -> :(
var passesValidation = !requiredAndNoValue; // true -> :)
return passesValidation;
}, '');
Which all operates like this:
Solution
So, what have we learned? Well, if we want the same rule to appear multiple times on the same element, the adapter would have to see the exact set of rules multiple times per element, with no way to differentiate between each instance within multiple sets. Further, ASP.NET won't render the same attribute name multiple times since it's not valid html.
So, we either need to:
Collapse all the client side rules into a single mega attribute with all the info
Rename attributes with each instance number and then find a way to parse them in sets.
I'll explore Option One (emitting a single client side attribute), which you could do a couple ways:
Create a single Attribute that takes in multiple elements to validate on the server client
Keep multiple distinct server side attributes and then merge all attributes via reflection before emitting to the client
In either case you will have to re-write the client side logic (adapter/method) to take an array of values, instead of a single value at a time.
To we'll build/transmit a JSON serialized object that looks like this:
var props = [
{
PropName: "RoleCd",
CompValues: ["2","3","4","5"]
},
{
PropName: "IsPatient",
CompValues: ["true"]
}
]
Scripts/ValidateRequiredIfAny.js
Here's how we'll handle that in client side adapter / method:
// hook up to client side validation
$.validator.unobtrusive.adapters.add("requiredifany", ["props"], function (options) {
options.rules["requiredifany"] = { props: options.params.props };
options.messages["requiredifany"] = options.message;
});
// test validity
$.validator.addMethod("requiredifany", function (value, element, params) {
var reqIfProps = JSON.parse(params.props);
var anytargetHasValue = false;
$.each(reqIfProps, function (index, item) {
var targetSel = "#" + buildTargetId(element, item.PropName);
var $targetEl = $(targetSel);
var targetHasValue = elHasValue($targetEl, item.CompValues);
if (targetHasValue) {
anytargetHasValue = true;
return ;
}
});
var valueRequired = anytargetHasValue;
var requiredAndNoValue = valueRequired && !value; // true -> :(
var passesValidation = !requiredAndNoValue; // true -> :)
return passesValidation;
}, "");
// UTILITY METHODS
function buildTargetId(currentElement, targetPropName) {
// https://stackoverflow.com/a/39725539/1366033
// we are only provided the name of the target property
// we need to build it's ID in the DOM based on a couple assumptions
// derive the stacking context and depth based on the current element's ID/name
// append the target property's name to that context
// currentElement.name i.e. Details[0].DosesRequested
var curId = currentElement.id; // get full id i.e. Details_0__DosesRequested
var context = curId.replace(/[^_]+$/, ""); // remove last prop i.e. Details_0__
var targetId = context + targetPropName; // build target ID i.e. Details_0__OrderIncrement
// fail noisily
if ($("#" + targetId).length === 0)
console.error(
"Could not find id '" + targetId +
"' when looking for '" + targetPropName +
"' on originating element '" + curId + "'");
return targetId;
}
function elHasValue($el, values) {
var isCheckBox = $el.is(":checkbox,:radio");
var isChecked = $el.is(":checked");
var inputValue = $el.val();
var valueInArray = $.inArray(String(inputValue), values) > -1;
var hasValue = (!isCheckBox || isChecked) && valueInArray;
return hasValue;
};
Models/RequiredIfAttribute.cs
On the server side, we'll validate attributes like normal, but when we got to build the client side attributes, we'll look for all attributes and build one mega attribute
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Web.Helpers;
using System.Web.Mvc;
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class RequiredIfAttribute : ValidationAttribute, IClientValidatable
{
public PropertyNameValues TargetProp { get; set; }
public RequiredIfAttribute(string compPropName, string[] compPropValues, string msg) : base(msg)
{
this.TargetProp = new PropertyNameValues()
{
PropName = compPropName,
CompValues = compPropValues
};
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
PropertyInfo compareProp = validationContext.ObjectType.GetProperty(TargetProp.PropName);
var compPropVal = compareProp.GetValue(validationContext.ObjectInstance, null);
string compPropValAsString = compPropVal?.ToString().ToLower() ?? "";
var matches = TargetProp.CompValues.Where(v => v == compPropValAsString);
bool needsValue = matches.Any();
if (needsValue)
{
if (value == null || value.ToString() == "" || value.ToString() == "0")
{
return new ValidationResult(FormatErrorMessage(null));
}
}
return ValidationResult.Success;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
// at this point, who cares that we're on this particular instance - find all instances
PropertyInfo curProp = metadata.ContainerType.GetProperty(metadata.PropertyName);
RequiredIfAttribute[] allReqIfAttr = curProp.GetCustomAttributes<RequiredIfAttribute>().ToArray();
// emit validation attributes from all simultaneously, otherwise each will overwrite the last
PropertyNameValues[] allReqIfInfo = allReqIfAttr.Select(x => x.TargetProp).ToArray();
string allReqJson = Json.Encode(allReqIfInfo);
var modelClientValidationRule = new ModelClientValidationRule
{
ValidationType = "requiredifany",
ErrorMessage = ErrorMessageString
};
// add name for jQuery parameters for the adapter, must be LOWERCASE!
modelClientValidationRule.ValidationParameters.Add("props", allReqJson);
return new List<ModelClientValidationRule> { modelClientValidationRule };
}
}
public class PropertyNameValues
{
public string PropName { get; set; }
public string[] CompValues { get; set; }
}
Then we can bind that to our model by applying multiple attributes simultaneously:
[RequiredIf(nameof(RelationshipCd), new[] { 1,2,3,4,5 }, "Mailing Address is required.")]
[RequiredIf(nameof(IsPatient), new[] { "true" },"Mailing Address is required.")]
public string MailingAddressLine1 { get; set; }
Further Reading
ASP.NET MVC custom multiple fields validation by Stephen Muecke
Unobtrusive Client Validation in ASP.NET MVC 3 by Brad Wilson
Finally here I found the answer my-self.
Look at following article for solution
http://www.codeproject.com/KB/validation/MultipleDataAnnotations.aspx
The link in the accepted answer (http://www.codeproject.com/KB/validation/MultipleDataAnnotations.aspx) is buggy, and someone else has written an errata here which I would recommend reading first. The answer above does not handle inheritance.
I believe this alternate solution has some advantages (including support of inheritance), but remains far from perfect code - improvements appreciated.
this C# uses Json.NET and Stuart Leeks HTML Attribute provider
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Mvc;
using Newtonsoft.Json;
namespace DabTrial.Infrastructure.Validation
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public abstract class MultipleValidationAttribute : ValidationAttribute, IMetadataAware
{
private class Validation
{
public ICollection<string> ErrorMessage { get; set; }
public IDictionary<string, ICollection<object>> Attributes { get; set; }
}
private object _typeId = new object();
public const string attributeName = "multipleValidations";
public MultipleValidationAttribute()
{
}
public override object TypeId
{
get
{
return this._typeId;
}
}
public void OnMetadataCreated(ModelMetadata metadata)
{
Dictionary<string, Validation> allMultis;
if (metadata.AdditionalValues.ContainsKey(attributeName))
{
allMultis = (Dictionary<string, Validation>)metadata.AdditionalValues[attributeName];
}
else
{
allMultis = new Dictionary<string, Validation>();
metadata.AdditionalValues.Add(attributeName, allMultis);
}
foreach (var result in GetClientValidationRules(metadata))
{
if (allMultis.ContainsKey(result.ValidationType))
{
var thisMulti = allMultis[result.ValidationType];
thisMulti.ErrorMessage.Add(result.ErrorMessage);
foreach (var attr in result.ValidationParameters)
{
thisMulti.Attributes[attr.Key].Add(attr.Value);
}
}
else
{
var thisMulti = new Validation
{
ErrorMessage = new List<string>(),
Attributes = new Dictionary<string, ICollection<object>>()
};
allMultis.Add(result.ValidationType, thisMulti);
thisMulti.ErrorMessage.Add(result.ErrorMessage);
foreach (var attr in result.ValidationParameters)
{
var newList = new List<object>();
newList.Add(attr.Value);
thisMulti.Attributes.Add(attr.Key, newList);
}
}
}
}
public static IEnumerable<KeyValuePair<string, object>> GetAttributes(ModelMetadata metadata)
{
if (!metadata.AdditionalValues.ContainsKey(attributeName))
{
return null;
}
var returnVar = new List<KeyValuePair<string, object>>();
returnVar.Add(new KeyValuePair<string,object>("data-val", true));
var allMultis = (Dictionary<string, Validation>)metadata.AdditionalValues[attributeName];
foreach (var multi in allMultis)
{
string valName = "data-val-" + multi.Key;
returnVar.Add(new KeyValuePair<string,object>(valName, JsonConvert.SerializeObject(multi.Value.ErrorMessage)));
returnVar.AddRange(multi.Value.Attributes.Select(a=>new KeyValuePair<string,object>(valName + '-' + a.Key, JsonConvert.SerializeObject(a.Value))));
}
return returnVar;
}
public virtual IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata,
ControllerContext context)
{
throw new NotImplementedException("This function must be overriden");
}
public virtual IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata)
{
return GetClientValidationRules(metadata, null);
}
}
}
the Global.asax contains the code
HtmlAttributeProvider.Register((metadata) =>
{
return MultipleValidationAttribute.GetAttributes(metadata);
});
and the JavaScript (within a custom validators function)
function setMultiValidationValues(options, ruleName, values) {
var i = 0, thisRule;
for (; i < values.length; i++) {
thisRule = (i == 0) ? ruleName : ruleName + i;
options.messages[thisRule] = values[i].message;
delete values[i].message;
options.rules[thisRule] = values[i];
if (ruleName !== thisRule) {
(function addValidatorMethod() {
var counter = 0;
if (!$.validator.methods[ruleName]) {
if (++counter > 10) { throw new ReferenceError(ruleName + " is not defined"); }
setTimeout(addValidatorMethod, 100);
return;
}
if (!$.validator.methods[thisRule]) { $.validator.addMethod(thisRule, $.validator.methods[ruleName]); }
})();
}
}
}
function transformValidationValues(options) {
var rules = $.parseJSON(options.message),
propNames = [], p, utilObj,i = 0,j, returnVar=[];
for (p in options.params) {
if (options.params.hasOwnProperty(p)) {
utilObj = {};
utilObj.key = p;
utilObj.vals = $.parseJSON(options.params[p]);
propNames.push(utilObj);
}
}
for (; i < rules.length; i++) {
utilObj = {};
utilObj.message = rules[i];
for (j=0; j < propNames.length; j++) {
utilObj[propNames[j].key] = propNames[j].vals[i];
}
returnVar.push(utilObj);
}
return returnVar;
}
An example of its use is below:
C#
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using System.Web.Mvc;
namespace DabTrial.Infrastructure.Validation
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class RegexCountAttribute : MultipleValidationAttribute
{
# region members
private string _defaultErrorMessageFormatString;
protected readonly string _regexStr;
protected readonly RegexOptions _regexOpt;
private int _minimumCount=0;
private int _maximumCount=int.MaxValue;
#endregion
#region properties
public int MinimumCount
{
get { return _minimumCount; }
set
{
if (value < 0) { throw new ArgumentOutOfRangeException(); }
_minimumCount = value;
}
}
public int MaximumCount
{
get { return _maximumCount; }
set
{
if (value < 0) { throw new ArgumentOutOfRangeException(); }
_maximumCount = value;
}
}
private string DefaultErrorMessageFormatString
{
get
{
if (_defaultErrorMessageFormatString == null)
{
_defaultErrorMessageFormatString = string.Format(
"{{0}} requires a {0}{1}{2} match(es) to regex {3}",
MinimumCount>0?"minimum of "+ MinimumCount:"",
MinimumCount > 0 && MaximumCount< int.MaxValue? " and " : "",
MaximumCount<int.MaxValue?"maximum of "+ MaximumCount:"",
_regexStr);
}
return _defaultErrorMessageFormatString;
}
set
{
_defaultErrorMessageFormatString = value;
}
}
#endregion
#region instantiation
public RegexCountAttribute(string regEx, string defaultErrorMessageFormatString = null, RegexOptions regexOpt = RegexOptions.None)
{
#if debug
if (minimumCount < 0) { throw new ArgumentException("the minimum value must be non-negative"); }
#endif
_regexStr = regEx;
DefaultErrorMessageFormatString = defaultErrorMessageFormatString;
_regexOpt = regexOpt;
}
#endregion
#region methods
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
var instr = (string)value;
int matchCount = 0;
if (MinimumCount > 0 && instr != null)
{
Match match = new Regex(_regexStr,_regexOpt).Match(instr);
while (match.Success && ++matchCount < MinimumCount)
{
match = match.NextMatch();
}
if (MaximumCount != int.MaxValue)
{
while (match.Success && ++matchCount <= MaximumCount)
{
match = match.NextMatch();
}
}
}
if (matchCount >= MinimumCount && matchCount <=MaximumCount)
{
return ValidationResult.Success;
}
string errorMessage = GetErrorMessage(validationContext.DisplayName);
return new ValidationResult(errorMessage);
}
protected string GetErrorMessage(string displayName)
{
return ErrorMessage ?? string.Format(DefaultErrorMessageFormatString,
displayName,
MinimumCount);
}
private bool HasFlag(RegexOptions options, RegexOptions flag)
{
return ((options & flag) == flag);
}
private string RegexpModifier
{
get
{
string options = string.Empty;
if (HasFlag(_regexOpt, RegexOptions.IgnoreCase)) { options += 'i'; }
if (HasFlag(_regexOpt, RegexOptions.Multiline)) { options += 'm'; }
return options;
}
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata)
{
var returnVal = new ModelClientValidationRule {
ErrorMessage = GetErrorMessage(metadata.DisplayName),
ValidationType = "regexcount",
};
returnVal.ValidationParameters.Add("min",MinimumCount);
returnVal.ValidationParameters.Add("max",MaximumCount);
returnVal.ValidationParameters.Add("regex",_regexStr);
returnVal.ValidationParameters.Add("regexopt", RegexpModifier);
yield return returnVal;
}
#endregion
}
public class MinNonAlphanum : RegexCountAttribute
{
public MinNonAlphanum(int minimum) : base("[^0-9a-zA-Z]", GetDefaultErrorMessageFormatString(minimum))
{
this.MinimumCount = minimum;
}
private static string GetDefaultErrorMessageFormatString(int min)
{
if (min == 1)
{
return "{0} requires a minimum of {1} character NOT be a letter OR number";
}
return "{0} requires a minimum of {1} characters NOT be a letter OR number";
}
}
public class MinDigits : RegexCountAttribute
{
public MinDigits(int minimum) : base(#"\d", GetDefaultErrorMessageFormatString(minimum))
{
this.MinimumCount = minimum;
}
private static string GetDefaultErrorMessageFormatString(int min)
{
if (min == 1)
{
return "{0} requires a minimum of {1} character is a number";
}
return "{0} requires a minimum of {1} characters are numbers";
}
}
}
JavaScript:
$.validator.addMethod("regexcount", function (value, element, params) {
var matches = (value.match(params.regex)||[]).length
return matches >= params.min && matches <= params.max;
});
$.validator.unobtrusive.adapters.add("regexcount", ["min", "max", "regex", "regexopt"], function (options) {
var args = transformValidationValues(options), i=0;
for (; i < args.length; i++) {
args[i].regex = new RegExp(args[i].regex, args[i].regexopt);
delete args[i].regexopt;
}
setMultiValidationValues(options, "regexcount", args);
});

In ASP.NET MVC 2, can you use data annotations to compare two fields in a form? What are the alternatives if not Data Annotations

I have two fields in my form
AccountNumber
ReverseAccountNumber
Can i use data annotations to validate that the value of "ReverseAccountNumber" textbox is equal to the reversed value of "AccountNumber".
i.e.
AccountNumber = 12345
ReverseAccountNumber = 54321
i expect the validation to occur on the lostFocus event of the ReverseAccountNumber textbox.
I think i can do this using IDataErrorInfo, However I believe this would require a POST first before validation occurs, and i consider it a last resort.
Simply add a validation attribute to the class (not the properties) and evaluate the class object to compare the two properties. As for the client side, ASP.NET MVC 3 should be able to generate proper client-side validation for this (although I have not tried it myself since Iam still using xVal).
CustomAttribute
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public sealed class ReversStringMatchAttribute : ValidationAttribute
{
public string Property { get; set; }
public ReversStringMatchAttribute()
{ }
public override bool IsValid(object value)
{
return true;
}
}
CustomValidator
public class ReversStringValidator : DataAnnotationsModelValidator<ReversStringMatchAttribute>
{
string property;
public ReversStringValidator(ModelMetadata metadata, ControllerContext context, ReversStringMatchAttribute attribute)
: base(metadata, context, attribute)
{
property = attribute.Property;
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
var rule = new ModelClientValidationRule
{
ErrorMessage = Attribute.ErrorMessage,
ValidationType = "reversStringValidator"
};
rule.ValidationParameters.Add("propertyname", property);
return new[] { rule };
}
}
Java Script
Sys.Mvc.ValidatorRegistry.validators["reversStringValidator"] = function (rule) {
//initialization
//return validator function
return function (value, context) {
var field = $get(rule.ValidationParameters['propertyname']);
if (field == null)
return "Property name is invalid!";
var s1 = field.value;
if (s1) {
if (value) {
var reverse = value.split("").reverse().join("");
if (s1 != reverse.toString()) {
return rule.ErrorMessage;
}
} else {
return rule.ErrorMessage;
}
}
return true;
}
};
then use it on your property
public class AccountViewModel
{
[Required(ErrorMessage="Account Number is Required")]
public string AccountNumber { get; set; }
[ReversStringMatch(ErrorMessage = "The value doesn't match the Account Number", Property="AccountNumber")]
public string ReverseAccountNumber { get; set; }
}
i have some doubts on the $get validation method in javascript but it works, for now.

Categories