I am coding a custom validation to my .NET MVC 4 application. This is the first validation that uses a parameter, and I'm finding some trouble to get this.
This is my C# code:
ValidationAttribute:
namespace Validations
{
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class MinRequiredPasswordLengthValidationAttribute : ValidationAttribute, IClientValidatable
{
public MinRequiredPasswordLengthValidationAttribute()
: base("The password is invalid")
{
}
public override string FormatErrorMessage(string name)
{
return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name.ToLower(), Membership.MinRequiredPasswordLength);
}
public override bool IsValid(object value)
{
if (value == null || String.IsNullOrEmpty(value.ToString()))
{
return true;
}
return value.ToString().Length >= Membership.MinRequiredPasswordLength;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
ModelClientMinRequiredPasswordLengthValidationRule rule = new ModelClientMinRequiredPasswordLengthValidationRule(FormatErrorMessage(metadata.GetDisplayName()), Membership.MinRequiredPasswordLength);
yield return rule;
}
}
}
ModelClientValidationRule:
namespace Validations
{
public class ModelClientMinRequiredPasswordLengthValidationRule : ModelClientValidationRule
{
public ModelClientMinRequiredPasswordLengthValidationRule(string errorMessage, int minRequiredPasswordLength)
{
ErrorMessage = errorMessage;
ValidationType = "minrequiredpasswordlength";
ValidationParameters.Add("minlength", minRequiredPasswordLength);
}
}
}
And here is the JS code, my problem:
jQuery.validator.addMethod("minrequiredpasswordlength", function (value, element, params) {
if (value.length == 0) {
return true; //empty
}
return true; //dummy return
}, "");
jQuery.validator.unobtrusive.adapters.add("minrequiredpasswordlength", {}, function (options) {
//XXX Here I should get the minlength parameter, but 'options' is a empty object
options.messages["minrequiredpasswordlength"] = options.message;
});
Many thanks for the help!
see if this link helps:
ASP.NET MVC 3 client-side validation with parameters
Also, try changing your javascript code to this:
$.validator.addMethod('minrequiredpasswordlength', function (value, element, params) {
var minLength = params.minLength;
// minLength should contain your value.
return true; // dummy return
});
jQuery.validator.unobtrusive.adapters.add('minrequiredpasswordlength', [ 'minlength' ], function (options) {
options.rules['minrequiredpasswordlength'] = {
minLength: options.params['minlength']
};
//XXX Here I should get the minlength parameter, but 'options' is a empty object
options.messages["minrequiredpasswordlength"] = options.message;
});
The secret here is to use [ 'minlength' ] instead of {} It took me a lot of time when I first learnt this. I hope this helps.
Related
I am writing a custom attribute to validate that a first and last name does not exceed a certain amount of characters, but the error message is not displaying like it does for out-of-the-box annotations.
Here is my implementation.
public class User
{
[Required(ErrorMessage = "Last Name is required.")]
[RegularExpression(#"^[a-zA-Z'\s]{1,50}$", ErrorMessage = "Please enter a valid last name.")]
[FullNameMaxLength("FirstName")]
public string LastName { get; set; }
}
public class FullNameMaxLengthAttribute : ValidationAttribute
{
private string _firstName;
public FullNameMaxLengthAttribute(string firstName)
{
_firstName = firstName;
}
protected override ValidationResult IsValid(object lastName, ValidationContext validationContext)
{
clsUserRegistration userRegistrationContext = (clsUserRegistration)validationContext.ObjectInstance;
if (lastName != null)
{
string strValue = lastName.ToString();
PropertyInfo propInfo = validationContext.ObjectType.GetProperty(_firstName);
if (propInfo == null)
return new ValidationResult(String.Format("Property {0} is undefined.", _firstName));
var fieldValue = propInfo.GetValue(validationContext.ObjectInstance, null).ToString();
if (strValue.Length + fieldValue.Length > 53)
{
return new ValidationResult("First and last names are too long!");
}
return ValidationResult.Success;
}
return null;
}
}
In my view, I have a ValidationMessageFor, and it works fine with non-custom attributes. When I step through my model, it returns the ValidationMessage, but I cannot see that error message. Any thoughts?
The above is just the "back-end" validation. This for example does still work when user's browser has JavaScript turned off - the page will post back regardless of errors but then show the form again with validation messages on it.
For "front-end" validation, you need something along these lines:
public class FullNameMaxLengthAttribute : ValidationAttribute, IClientValidatable
{
// Your Properties and IsValid method here
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ErrorMessage = String.IsNullOrEmpty(ErrorMessage) ? FormatErrorMessage(metadata.DisplayName) : ErrorMessage,
ValidationType = "fullnamemaxlength"
};
rule.ValidationParameters["firstname"] = FirstName;
rule.ValidationParameters["maxlength"] = 53;
yield return rule;
}
}
And then in JavaScript that is added to the page:
if (jQuery.validator) {
jQuery.validator.addMethod("fullnamemaxlength", function(value, element, param) {
var name = param.firstname;
var max = param.maxlength;
return name.length > max;
});
jQuery.validator.unobtrusive.adapters.add("fullnamemaxlength", ["firstname", "maxlength"], function (options) {
options.rules.fullnamemaxlength = {};
options.rules.fullnamemaxlength.firstname = options.params.firstname;
options.rules.fullnamemaxlength.maxlength = options.params.maximum;
options.messages.fullnamemaxlength = options.message;
}
);
}
Note this sits OUTSIDE of document.ready() { };
Something similar here: client-side validation in custom validation attribute - asp.net mvc 4
Why is my client side validation not working.
Property is:
[Required(ErrorMessage = "Email address is required")]
[EmailAddress(ErrorMessage = "Invalid Email Address")]
[NonCompanyEmailAttribute(ErrorMessage = "Email address of customer required (not company employees)")]
public string EmailAddress
The validation attribute is:
public class NonCompanyEmailAttribute : ValidationAttribute, IClientValidatable
{
public override bool IsValid(object value)
{
bool containscompany = !((string)value).ToLower().Contains("#company.com");
return containscompany;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
yield return new ModelClientValidationRule
{
ErrorMessage = this.ErrorMessage,
ValidationType = "noncompanyemail"
};
}
}
In JS I have:
$.validator.addMethod("noncompanyemail", function (value, element, params) {
return value.toLowerCase().indexOf('company.com' > -1);
});
$.validator.unobtrusive.adapters.add("noncompanyemail", function (options) {
options.rules["noncompanyemail"] = true;
options.messages["noncompanyemail"] = options.message;
});
jquery.validate.js and jquery.validate.unobtrusive.js are included
I'm not sure what you mean by "not working" but your jquery validation function appears to be oddly formed. I assume you want it to return false (aka invalid) if the value contains company.com anywhere. If so, your method should be something like:
$.validator.addMethod("noncompanyemail", function (value, element, params) {
return (value.toLowerCase().indexOf('company.com') == -1);
});
I've seen a lot of similar posts on this, but haven't found the answer specific to controller parameters.
I've written a custom attribute called AliasAttribute that allows me to define aliases for parameters during model binding. So for example if I have: public JsonResult EmailCheck(string email) on the server and I want the email parameter to be bound to fields named PrimaryEmail or SomeCrazyEmail I can "map" this using the aliasattribute like this: public JsonResult EmailCheck([Alias(Suffix = "Email")]string email).
The problem: In my custom model binder I can't get a hold of the AliasAttribute class applied to the email parameter. It always returns null.
I've seen what the DefaultModelBinder class is doing to get the BindAttribute in reflector and its the same but doesn't work for me.
Question: How do I get this attribute during binding?
AliasModelBinder:
public class AliasModelBinder : DefaultModelBinder
{
public static ICustomTypeDescriptor GetTypeDescriptor(Type type)
{
return new AssociatedMetadataTypeTypeDescriptionProvider(type).GetTypeDescriptor(type);
}
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = base.BindModel(controllerContext, bindingContext);
var descriptor = GetTypeDescriptor(bindingContext.ModelType);
/*************************/
// this next statement returns null!
/*************************/
AliasAttribute attr = (AliasAttribute)descriptor.GetAttributes()[typeof(AliasAttribute)];
if (attr == null)
return null;
HttpRequestBase request = controllerContext.HttpContext.Request;
foreach (var key in request.Form.AllKeys)
{
if (string.IsNullOrEmpty(attr.Prefix) == false)
{
if (key.StartsWith(attr.Prefix, StringComparison.InvariantCultureIgnoreCase))
{
if (string.IsNullOrEmpty(attr.Suffix) == false)
{
if (key.EndsWith(attr.Suffix, StringComparison.InvariantCultureIgnoreCase))
{
return request.Form.Get(key);
}
}
return request.Form.Get(key);
}
}
else if (string.IsNullOrEmpty(attr.Suffix) == false)
{
if (key.EndsWith(attr.Suffix, StringComparison.InvariantCultureIgnoreCase))
{
return request.Form.Get(key);
}
}
if (attr.HasIncludes)
{
foreach (var include in attr.InlcludeSplit)
{
if (key.Equals(include, StringComparison.InvariantCultureIgnoreCase))
{
return request.Form.Get(include);
}
}
}
}
return null;
}
}
AliasAttribute:
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class AliasAttribute : Attribute
{
private string _include;
private string[] _inlcludeSplit = new string[0];
public string Prefix { get; set; }
public string Suffix { get; set; }
public string Include
{
get
{
return _include;
}
set
{
_include = value;
_inlcludeSplit = SplitString(_include);
}
}
public string[] InlcludeSplit
{
get
{
return _inlcludeSplit;
}
}
public bool HasIncludes { get { return InlcludeSplit.Length > 0; } }
internal static string[] SplitString(string original)
{
if (string.IsNullOrEmpty(original))
{
return new string[0];
}
return (from piece in original.Split(new char[] { ',' })
let trimmed = piece.Trim()
where !string.IsNullOrEmpty(trimmed)
select trimmed).ToArray<string>();
}
}
Usage:
public JsonResult EmailCheck([ModelBinder(typeof(AliasModelBinder)), Alias(Suffix = "Email")]string email)
{
// email will be assigned to any field suffixed with "Email". e.g. PrimaryEmail, SecondaryEmail and so on
}
Gave up on this and then stumbled across the Action Parameter Alias code base that will probably allow me to do this. It's not as flexible as what I started out to write but probably can be modified to allow wild cards.
what I did was make my attribute subclass System.Web.Mvc.CustomModelBinderAttribute which then allows you to return a version of your custom model binder modified with the aliases.
example:
public class AliasAttribute : System.Web.Mvc.CustomModelBinderAttribute
{
public AliasAttribute()
{
}
public AliasAttribute( string alias )
{
Alias = alias;
}
public string Alias { get; set; }
public override IModelBinder GetBinder()
{
var binder = new AliasModelBinder();
if ( !string.IsNullOrEmpty( Alias ) )
binder.Alias = Alias;
return binder;
}
}
which then allows this usage:
public ActionResult Edit( [Alias( "somethingElse" )] string email )
{
// ...
}
Here's the setup:
I have some MVC Controllers that are intended to be consumed by jQuery ajax requests. A normal request would seem somewhat like this:
$.ajax("/Solicitor/AddSolicitorToApplication", {
data: putData,
type: "POST", contentType: "application/json",
success: function (result) {
//My success callback
}
}
});
My controller looks like this:
[HttpPost]
public ActionResult InsertLoanApplication(MortgageLoanApplicationViewModel vm)
{
var mortgageLoanDTO = vm.MapToDTO();
return Json(_mortgageLoanService.UpdateMortgageLoanApplication(mortgageLoanDTO), JsonRequestBehavior.DenyGet);
}
This works perfectly fine with most objects passed to the controller, except that in this specific case one of the properties of the object being passed needs to be deserialized in a specific way.
I've added a JsonConverter that I've used previously with the MVC4 Web API, but in this case I need to apply it to regular mvc controllers.
I tried registering the JsonConverter in my global.asax like this:
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new GrizlyStringConverter());
But so far haven't been able to deserialize the object.
You should replace the built-in JsonValueProviderFactory class with a custom one if you want to use Json.NET when binding JSON requests to view models.
You could write one as shown in this gist:
public sealed class JsonDotNetValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
{
return null;
}
using (var reader = new StreamReader(controllerContext.HttpContext.Request.InputStream))
{
var bodyText = reader.ReadToEnd();
return String.IsNullOrEmpty(bodyText)
? null :
new DictionaryValueProvider<object>(
JsonConvert.DeserializeObject<ExpandoObject>(
bodyText,
new ExpandoObjectConverter()
),
CultureInfo.CurrentCulture
);
}
}
}
and then replace the built-in with your custom one in Application_Start:
ValueProviderFactories.Factories.Remove(
ValueProviderFactories
.Factories
.OfType<JsonValueProviderFactory>()
.FirstOrDefault()
);
ValueProviderFactories.Factories.Add(new JsonDotNetValueProviderFactory());
That's it. Now you are using Json.Net instead of the JavaScriptSerializer for the incoming JSON requests.
The modified version:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using System.Globalization;
using System.IO;
using System.Web.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace MvcJsonNetTests.Utils
{
public class JsonNetValueProviderFactory : ValueProviderFactory
{
public JsonNetValueProviderFactory()
{
Settings = new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Error,
Converters = { new ExpandoObjectConverter() }
};
}
public JsonSerializerSettings Settings { get; set; }
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
if (controllerContext == null)
throw new ArgumentNullException("controllerContext");
if (controllerContext.HttpContext == null ||
controllerContext.HttpContext.Request == null ||
controllerContext.HttpContext.Request.ContentType == null)
{
return null;
}
if (!controllerContext.HttpContext.Request.ContentType.StartsWith(
"application/json", StringComparison.OrdinalIgnoreCase))
{
return null;
}
if (!controllerContext.HttpContext.Request.IsAjaxRequest())
{
return null;
}
using (var streamReader = new StreamReader(controllerContext.HttpContext.Request.InputStream))
{
using (var jsonReader = new JsonTextReader(streamReader))
{
if (!jsonReader.Read())
return null;
var jsonSerializer = JsonSerializer.Create(this.Settings);
Object jsonObject;
switch (jsonReader.TokenType)
{
case JsonToken.StartArray:
jsonObject = jsonSerializer.Deserialize<List<ExpandoObject>>(jsonReader);
break;
default:
jsonObject = jsonSerializer.Deserialize<ExpandoObject>(jsonReader);
break;
}
var backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
addToBackingStore(backingStore, String.Empty, jsonObject);
return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
}
}
}
private static void addToBackingStore(IDictionary<string, object> backingStore, string prefix, object value)
{
var dictionary = value as IDictionary<string, object>;
if (dictionary != null)
{
foreach (var entry in dictionary)
{
addToBackingStore(backingStore, makePropertyKey(prefix, entry.Key), entry.Value);
}
return;
}
var list = value as IList;
if (list != null)
{
for (var index = 0; index < list.Count; index++)
{
addToBackingStore(backingStore, makeArrayKey(prefix, index), list[index]);
}
return;
}
backingStore[prefix] = value;
}
private static string makeArrayKey(string prefix, int index)
{
return prefix + "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
}
private static string makePropertyKey(string prefix, string propertyName)
{
return (string.IsNullOrWhiteSpace(prefix)) ? propertyName : prefix + "." + propertyName;
}
}
}
Also to register it at the right index:
public static void RegisterFactory()
{
var defaultJsonFactory = ValueProviderFactories.Factories
.OfType<JsonValueProviderFactory>().FirstOrDefault();
var index = ValueProviderFactories.Factories.IndexOf(defaultJsonFactory);
ValueProviderFactories.Factories.Remove(defaultJsonFactory);
ValueProviderFactories.Factories.Insert(index, new JsonNetValueProviderFactory());
}
Since there is no way to validate a property (with unobtrusive clientside validation) using multiple regex patterns (because validation type has to be unique) i decided to extend FluentValidation so i can do the following.
RuleFor(x => x.Name).NotEmpty().WithMessage("Name is required")
.Length(3, 20).WithMessage("Name must contain between 3 and 20 characters")
.Match(#"^[A-Z]").WithMessage("Name has to start with an uppercase letter")
.Match(#"^[a-zA-Z0-9_\-\.]*$").WithMessage("Name can only contain: a-z 0-9 _ - .")
.Match(#"[a-z0-9]$").WithMessage("Name has to end with a lowercase letter or digit")
.NotMatch(#"[_\-\.]{2,}").WithMessage("Name cannot contain consecutive non-alphanumeric characters");
The last thing i need to figure out is how to pass the errormessage which is set using WithMessage() via GetClientValidationRules() so it ends up in the "data-val-customregex[SOMEFANCYSTRINGHERETOMAKEITUNIQUE]" attribute on the input element.
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) {
var rule = new ModelClientValidationRule();
rule.ErrorMessage = [INSERT ERRORMESSAGE HERE];
rule.ValidationType = "customregex" + StringFunctions.RandomLetters(6);
rule.ValidationParameters.Add("pattern", pattern);
yield return rule;
}
I've been looking at the FluentValidation sourcecode, but couldn't figure it out. Anyone got any ideas?
I've been discussing how to do this with Jeremy Skinner (the creator of Fluent Validation) at
http://fluentvalidation.codeplex.com/discussions/253505
He was kind enough to write a complete example.
Update
Here is the code we came up with:
First the extensions, for both Match and NotMatch.
public static class Extensions
{
public static IRuleBuilderOptions<T, string> Match<T>(this IRuleBuilder<T, string> ruleBuilder, string expression)
{
return ruleBuilder.SetValidator(new MatchValidator(expression));
}
public static IRuleBuilderOptions<T, string> NotMatch<T>(this IRuleBuilder<T, string> ruleBuilder, string expression) {
return ruleBuilder.SetValidator(new MatchValidator(expression, false));
}
}
The used interface for the validator
public interface IMatchValidator : IPropertyValidator
{
string Expression { get; }
bool MustMatch { get; }
}
The actual validator:
public class MatchValidator : PropertyValidator, IMatchValidator
{
string expression;
bool mustMatch;
public MatchValidator(string expression, bool mustMatch = true)
: base(string.Format("The value {0} match with the given expression, while it {1}.", mustMatch ? "did not" : "did", mustMatch ? "should" : "should not"))
{
this.expression = expression;
this.mustMatch = mustMatch;
}
protected override bool IsValid(PropertyValidatorContext context)
{
return context.PropertyValue == null ||
context.PropertyValue.ToString() == string.Empty ||
Regex.IsMatch(context.PropertyValue.ToString(), expression) == mustMatch;
}
public string Expression
{
get { return expression; }
}
public bool MustMatch {
get { return mustMatch; }
}
}
The adaptor to register the validator:
public class MatchValidatorAdaptor : FluentValidationPropertyValidator
{
public MatchValidatorAdaptor(ModelMetadata metadata, ControllerContext controllerContext, PropertyRule rule, IPropertyValidator validator)
: base(metadata, controllerContext, rule, validator)
{
}
IMatchValidator MatchValidator
{
get { return (IMatchValidator)Validator; }
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
var formatter = new MessageFormatter().AppendPropertyName(Rule.PropertyDescription);
string errorMessage = formatter.BuildMessage(Validator.ErrorMessageSource.GetString());
yield return new ModelClientValidationMatchRule(MatchValidator.Expression, MatchValidator.MustMatch, errorMessage);
}
}
And finally where the magic happens:
public class ModelClientValidationMatchRule : ModelClientValidationRule
{
public ModelClientValidationMatchRule(string expression, bool mustMatch, string errorMessage)
{
if (mustMatch)
base.ValidationType = "match";
else
base.ValidationType = "notmatch";
base.ValidationType += StringFunctions.RandomLetters(6);
base.ErrorMessage = errorMessage;
base.ValidationParameters.Add("expression", expression);
}
}
Update 2:
Javascript to wireup jQuery.validator:
(function ($) {
function attachMatchValidator(name, mustMatch) {
$.validator.addMethod(name, function (val, element, expression) {
var rg = new RegExp(expression, "gi");
return (rg.test(val) == mustMatch);
});
$.validator.unobtrusive.adapters.addSingleVal(name, "expression");
}
$("input[type=text]").each(function () {
$.each(this.attributes, function (i, attribute) {
if (attribute.name.length == 20 && attribute.name.substring(0, 14) == "data-val-match")
attachMatchValidator(attribute.name.substring(9, 20), true);
if (attribute.name.length == 23 && attribute.name.substring(0, 17) == "data-val-notmatch")
attachMatchValidator(attribute.name.substring(9, 23), false);
});
});
} (jQuery));
A little off topic, but maybe helpful. Regex are pretty powerful, have you considered combining all the rules in one regex? I think that's why the attributes providing regex validation usually don't allow multiple instances per property.
So for your example, your regex would be:
"^[A-Z]([a-zA-Z0-9][_\-\.]{0,1}[a-zA-Z0-9]*)*[a-z0-9]$"
And a handy place to test it: http://derekslager.com/blog/posts/2007/09/a-better-dotnet-regular-expression-tester.ashx