I am working with data annotations in my MVC 4 application to handle validation. One requirement of this was to fully localise the all error messages and the regular expressions.
In order to do this, I wrote an attribute extension, as below.
View:
#Html.LabelFor(m => m.Postcode, new { #class = "input-label", #for = "valid-postcode" })
#Html.TextBoxFor(m => m.Postcode, new { type = "text", id = "valid-postcode", autocomplete = "off" })
#Html.ValidationMessageFor(m => m.Postcode)
Model:
// Postcode
[Required(ErrorMessageResourceType = typeof(Resources.FormValidation), ErrorMessageResourceName = "requiredMsg")]
[Localised(typeof(Resources.FormValidation), "postcodeRegEx", "postcodeMsg")]
[Display(Name = "postcode", ResourceType = typeof(Resources.FormLabels))]
public string Postcode { get; set; }
Attribute Extension:
public class LocalisedAttribute : RegularExpressionAttribute
{
public LocalisedAttribute(Type resource, string regularExpression, string errorMessage)
: base(Resources.FormValidation.ResourceManager.GetString(regularExpression))
{
ErrorMessageResourceType = resource;
ErrorMessageResourceName = errorMessage;
}
}
If I set a breakpoint on my Attribute Extension and start the application, I hit the break point when I view the page that contains my form elements. As I test I added an additional field that also uses the same extension and it hit the breakpoint twice. So from this, I know it's working and I know it's getting my regular expressions from my resource files.
THE PROBLEM
I have a menu to switch the culture used in the application. When I choose a new culture and the page is refreshed, all references to my resource files used in my Views and the Display Name and Error Messages in data annotations pick up the culture change and use the correct resource file.
However, the regular expression is NOT updated and the breakpoint I set is not hit again. This means my extension is still using the regex it picked up when it was hit and therefore doesn't validate correctly.
I can post more details on how the culture is changed from this menu if needed, but the basic structure is
A controller that changes the culture and returns the user to the same page
Modified my routing to include the culture e.g. www.site.com/en-GB/View
What I need is for my attribute extension to be hit every time the culture is switched, and not just the first time the application is started.
Is this possible, or should I be revising my whole approach?
Your menu strings are referenced at runtime, while your attribute is compiled before your app runs
I can understand why this is confusing.
Resource files are fundamentally meant to be used for dynamic behavior. And string values can be altered at run time.
But, when we dig into the usage of this particular string, you are using Resources.FormValidation.ResourceManager.GetString(regularExpression) resource string as part of the compile instruction to create Postcode. The Razor Framework will use this data to create annotation templates for validation.
[Required(ErrorMessageResourceType = typeof(Resources.FormValidation), ErrorMessageResourceName = "requiredMsg")]
[Localised(typeof(Resources.FormValidation), "postcodeRegEx", "postcodeMsg")]
[Display(Name = "postcode", ResourceType = typeof(Resources.FormLabels))]
public string Postcode { get; set; }
You are using this string the postcodeRegEx string at COMPILE TIME:
In some cases, compiled and pre-compiled code dependent on string literals can behave differently if the string changes. In other cases, like validation attribute behaviors, you do not get to "re-compile" your object's behavior so easily.
Possible Solutions
To achieve this kind of "end-around", you have to go outside the standard
1) implement an extension to validation attribute (ValidationAttribute), inheriting from RegularExpressionAttribute which reads the specific RegEx string from your resource file and passes it to the base RegEx Attribute.
// New attribute loads RegEx when needed
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class LocalisedAttribute : RegularExpressionAttribute
{
static LocalizedRegexAttribute()
{
// necessary to enable client side validation
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(LocalizedRegexAttribute), typeof(RegularExpressionAttributeAdapter));
}
public LocalisedAttribute(Type resource, string regularExpressionKey, string errorMessage)
: base(LoadRegex(regularExpressionKey))
{
ErrorMessageResourceType = resource;
ErrorMessageResourceName = errorMessage;
}
private static string LoadRegex(string key)
{
var resourceManager = new ResourceManager(typeof(Resources.FormValidation));
return resourceManager.GetString(key);
}
}
2) use JQuery to make input's data-val-regex-pattern = #ViewBag.RegEx
It will reference the JQuery function
$.validator.unobtrusive.adapters.add('Postcode ', function(options) { /*...*/ });
And I suspect the data-val-regex-pattern for the Postcode input will be set to the value from your initial resource file.
Related
I have created a custom CompareLessThan validation attribute by copying the ASP.NET MVC 3 CompareAttribute and instead of checking for equality, I check to see that one property is less than another. If there is a client side error, the message '{0} must be less than {1}' is displayed to the user.
My model is setup as follows with the Display attributes referencing a resource file.
[CompareLessThan("AmountAvailable", ErrorMessageResourceName="CompareLessThan", ErrorMessageResourceType = typeof(Resources.ValidationMessages))]
[Display(Name = "Amount", ResourceType = typeof(Resources.Labels))]
public decimal Amount { get; set; }
[Display(Name = "AmountAvailable", ResourceType = typeof(Resources.Labels))]
public decimal AmountAvailable { get; set; }
Then the custom validation GetClientValidationRules method is exactly the same as in the CompareAttribute
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
yield return new ModelClientValidationLessThanRule(FormatErrorMessage(metadata.DisplayName), FormatPropertyForClientValidation(OtherProperty), this.AllowEquality);
}
Here we are generating the error message that will be displayed to the user if there is a problem. I can get the display name from the resource file for the property that is decorated with my custom CompareLessThan attribute, but my question is how do I go about getting the display name of the 'other' property we are comparing against? In the IsValid method we have a reference to the validationContext from which I can generate a PropertyInfo object for the 'other' property and I think get the display name. But, in the GetClientValidationRules I don't have access to that.
I could always just pass in another value for the display name of the other property but I was hoping there would be a way to derive it as I'm already specifying it with data annotations.
Any ideas?
As of ASP.NET MVC 4 this is how I managed to get the other property:
PropertyInfo otherPropertyInfo =
this.Metadata.ContainerType.GetProperty(attribute.DependentProperty);
Then I got the Display attribute from the property:
var displayAttribute =
otherPropertyInfo.GetCustomAttributes(typeof(DisplayAttribute), true).
FirstOrDefault() as DisplayProperty;
In your case:
// GetName() is important to get the translated name if you're using a resource file...
this.otherPropertyDisplayName = displayAttribute.GetName();
GetName() reference:
http://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations.displayattribute.name%28v=vs.95%29.aspx
The answer provided by nemesv didn't work as the metadata.Model property has a value of 0. But, through the metadata we do have the full name of the model so it is possible to create a new instance of that model and then create a new DataAnnonationsModelMetadataProvider from that create instance. From there we can get the display name of the other property.
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
Type type = Type.GetType(metadata.ContainerType.FullName);
var model = Activator.CreateInstance(type);
var provider = new DataAnnotationsModelMetadataProvider();
var otherMetaData = provider.GetMetadataForProperty(() => model, type, this.OtherProperty);
this.otherPropertyDisplayName = otherMetaData.DisplayName;
yield return new ModelClientValidationLessThanRule(FormatErrorMessage(metadata.DisplayName), FormatPropertyForClientValidation(this.OtherProperty), this.AllowEquality);
}
I really don't like this solution (even though it works) as it seems there should be a better way. Does anyone else have any other ideas?
I haven't tried it out but you can get the model properties with the metadata.Properties property
metadata.Properties.Single(p => p.PropertyName == "OtherPropName").DisplayName;
EDIT: Because Properties is empty what you can always do (although it's very elegant). You can generate the metadata for yourself.
var provider = new DataAnnotationsModelMetadataProvider();
var otherMetaData = provider.GetMetadataForProperty(() => metaData.Model, metaData.ModelType, "OtherPropertyName");
This is the problem that we're facing in .NET MVC 2.
We're trying to use DataAnnotations to take care of the Model Validation for us, like it's supposed to. The only problem that we're having is that we don't want the standard error messages (because we have multiple languages on our website).
We want to localize this, but the way the site is setup, is that all text comes from a database. So we'd like to have our error messages in the database as well.
So we wrote a custom RequiredAttribute, like this:
public class LocalizedRequiredAttribute : RequiredAttribute
{
public string LocalizedErrorMessage
{
get
{
return ErrorMessage;
}
set
{
ErrorMessage = value.Translate();
}
}
}
We wrote an extension to the String class to add the "Translate()" method, which does the necessary database lookup for the correct localized version.
We use our attribute like this:
[LocalizedRequired(LocalizedErrorMessage = "Naam is required")]
public string Name {get; set; }
This works, but only once.
If you visit the site in French first, you'll see the French error message stating that you're supposed to enter a value. If you visit the English site later, you'll still see the French error on the English page. The Setter seems to be called only once.
What can we do to prevent this behavior and refresh the error message every time the validation is run / the model is populated with values?
Thanks for any help you can give me.
Couldn't you fix this by moving your .Translate() from your setter to your getter? It makes sense that your setter is called only once.
Edit:
I assumed ErrorMessage was a virtual message, which is not the case.
Your only option might be to create Resource class (you don't need a resource file) that retrieves your values from the database.
[Required(ErrorMesageResourceName="FirstName", ErrorMessageResourceType=typeof(ABCResourceClass))]
public string Name {get; set; }
class ABCResourceClass{
public static String FirstName{
get{
return Translate("FirstName");
}
}
}
As you can infer from the example, the annotations framework calls the property with the name that matches the string you provide to ErrorMessageResourceName.
You could resort to some kindof code generation technique to create the ABCResourceClass if you have a lot of properties.
Just use method FormatErrorMessage() (whis is called everytime) to set ErrorMessage property
But it's hackish
public class ErrorLocalizedRequiredAttribute : RequiredAttribute
{
public ErrorLocalizedRequiredAttribute(string name)
{
Name = name;
}
public string Name
{ get; set; }
public override string FormatErrorMessage(string name)
{
//get translation from DB by Name
ErrorMessage = Localization.Translate(Name);
return base.FormatErrorMessage(name);
}
}
.
.
.
[ErrorLocalizedRequiredAttribute("EmailIsRequired")]
public string Email
{
get; set;
}
I'm doing some simple validation inside my Controller I know this would better placed inside something like a service layer but for this I want to keep it inside the Controller
The idea is to check that a valid url is being entered into a url field to display an image, e.g. http://domain.com/myawesomeimage.png would be valid and http://domain.com/ would not be valid.
// ValidateInt has a default value of 0
int ValidateInt = 0;
// If the url entered (if one at all) does not have correct extension then increment the ValidateInt
if (!ArticleToEdit.image.Contains(".jpg"))
ValidateInt++;
if (!ArticleToEdit.image.Contains(".jpeg"))
ValidateInt++;
if (!ArticleToEdit.image.Contains(".png"))
ValidateInt++;
if (!ArticleToEdit.image.Contains(".gif"))
ValidateInt++;
if (!ArticleToEdit.image.Contains(".bmp"))
ValidateInt++;
// if ValidateInt is bigger than 0 then the url is invalid
if (ValidateInt > 0)
ModelState.AddModelError("Image", "Please enter a valid URL.");
EDITED CODE
The problem with your code
Your code is invalid, because your model state will always have at least 4 errors. Even though the URL would be correct. Your code requires that your URL must have all extensions which is of course incorrect. It can only have one. At most.
The solution
Use DataAnnotations instead and use regular expression validator. You're obviously already using some application model class called ArticleToCreate. You'll have to put data annotations attribute on the image property (one more observation: keey property names with Pascal casing so it's the same as .net):
public class ArticleToCreate
{
[RegularExpression(#"...")] // add regular expression that fulfils your requirements
public string Image { get; set; }
...
}
Then it all depends how complicated your regular expression is. The easiest one for your needs could be just that it starts with an http:// and end with the correct extension:
^http:\/\/.+\.(?:png|jpe?g|gif|bmp)$
And if you're directly providing your class instance to controller action it will get automatically validated for you without any additional code. This way you won't be able to forget to validate your objects manually.
Consider this controller action that automatically validates your model class object instance by validators defined on it (as per validator definition I've written above):
public ActionResult Create(ArticleToCreate data)
{
if (!this.ModelState.IsValid)
{
// handle invalid object
}
// handle valid object
}
This way your actions will focus on the processing part which is their main objective instead of focusing on too many aspects of your business process like validation for instance.
Shorter code = simpler code = easier to maintain = less bugs = less work = happy clients
Why not create custom ValidationAttributes (from DataAnnotations) and allow the Validation Engine do the work for you rather than worrying about where to put your logic?
I'm guessing it would look something like:
[AttributeUsage(AttributeTargets.Property |
AttributeTargets.Field, AllowMultiple = false)]
public class ValidImageUrlAttribute : ValidationAttribute
{
public string ErrorMessage { get; set; }
public override bool IsValid(object value)
{
var url = value as string;
if(!url.Contains(".jpg") || !url.Contains(".jpeg")
|| !url.Contains(".gif") || !url.Contains(".bmp")
|| !url.Contains(".png"))
{
return false;
}
return true;
}
public override string FormatErrorMessage(string name)
{
return ErrorMessage ?? base.FormatErrorMessage(name);
}
}
And then you could decorate your Model:
[Required(ErrorMessage = "Image URL is required.")]
[ValidImageUrl(ErrorMessage = "Valid Image URL is required.")]
public string ImageUrl { get; set; }
changed if (validate1 > 0) and it works fine :)
I'm currently working in an MVC 2 app which has to have everything localized in n-languages (currently 2, none of them english btw). I validate my model classes with DataAnnotations but when I wanted to validate a DateTime field found out that the DataTypeAttribute returns always true, no matter it was a valid date or not (that's because when I enter a random string "foo", the IsValid() method checks against "01/01/0001 ", dont know why).
Decided to write my own validator extending ValidationAtribute class:
public class DateTimeAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
DateTime result;
if (value.ToString().Equals("01/01/0001 0:00:00"))
{
return false;
}
return DateTime.TryParse(value.ToString(), out result);
}
}
Now it checks OK when is valid and when it's not, but my problem starts when I try to localize it:
[Required(ErrorMessageResourceType = typeof(MSG), ErrorMessageResourceName = "INS_DATA_Required")]
[CustomValidation.DateTime(ErrorMessageResourceType = typeof(MSG), ErrorMessageResourceName = "INS_DATA_DataType")]
public DateTime INS_DATA { get; set; }
If I put nothing in the field I get a localized MSG (MSG being my resource class) for the key=INS_DATA_Required but if I put a bad-formatted date I get the "The value 'foo' is not valid for INS_DATA" default message and not the localized MSG.
What am I misssing?
It could be that your ToString() is using a 'localized' format so your hard coded string will not match.
try replacing your "if" condition with:
if((DateTime)value == DateTime.MinValue)
When I use UpdateModel or TryUpdateModel, the MVC framework is smart enough to know if you are trying to pass in a null into a value type (e.g. the user forgets to fill out the required Birth Day field) .
Unfortunately, I don't know how to override the default message, "A value is required." in the summary into something more meaningful ("Please enter in your Birth Day").
There has to be a way of doing this (without writing too much work-around code), but I can't find it. Any help?
EDIT
Also, I guess this would also be an issue for invalid conversions, e.g. BirthDay = "Hello".
Make your own ModelBinder by extending DefaultModelBinder:
public class LocalizationModelBinder : DefaultModelBinder
Override SetProperty:
base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
foreach (var error in bindingContext.ModelState[propertyDescriptor.Name].Errors.
Where(e => IsFormatException(e.Exception)))
{
if (propertyDescriptor.Attributes[typeof(TypeErrorMessageAttribute)] != null)
{
string errorMessage =
((TypeErrorMessageAttribute)propertyDescriptor.Attributes[typeof(TypeErrorMessageAttribute)]).GetErrorMessage();
bindingContext.ModelState[propertyDescriptor.Name].Errors.Remove(error);
bindingContext.ModelState[propertyDescriptor.Name].Errors.Add(errorMessage);
break;
}
}
Add the function bool IsFormatException(Exception e) to check if an Exception is a FormatException:
if (e == null)
return false;
else if (e is FormatException)
return true;
else
return IsFormatException(e.InnerException);
Create an Attribute class:
[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = false)]
public class TypeErrorMessageAttribute : Attribute
{
public string ErrorMessage { get; set; }
public string ErrorMessageResourceName { get; set; }
public Type ErrorMessageResourceType { get; set; }
public TypeErrorMessageAttribute()
{
}
public string GetErrorMessage()
{
PropertyInfo prop = ErrorMessageResourceType.GetProperty(ErrorMessageResourceName);
return prop.GetValue(null, null).ToString();
}
}
Add the attribute to the property you wish to validate:
[TypeErrorMessage(ErrorMessageResourceName = "IsGoodType", ErrorMessageResourceType = typeof(AddLang))]
public bool IsGood { get; set; }
AddLang is a resx file and IsGoodType is the name of the resource.
And finally add this into Global.asax.cs Application_Start:
ModelBinders.Binders.DefaultBinder = new LocalizationModelBinder();
Cheers!
With the DefaultModelBinder it is possible to override the default required error message but unfortunately it would apply globally which IMHO renders it completely useless. But in case you decide to do it here's how:
Add the App_GlobalResources folder to your ASP.NET site
Add a resources file called Messages.resx
Inside the resources file declare a new string resource with the key PropertyValueRequired and some value
In Application_Start add the following line:
DefaultModelBinder.ResourceClassKey = "Messages";
As you can see there's no link between the model property you are validating and the error message.
In conclusion it is better to write custom validation logic to handle this scenario. One way would be to use a nullable type (System.Nullable<TValueType>) and then:
if (model.MyProperty == null ||
/** Haven't tested if this condition is necessary **/
!model.MyProperty.HasValue)
{
ModelState.AddModelError("MyProperty", "MyProperty is required");
}
I've been using the awesome xVal validation framework. It lets me do all my validation in the model (Even LINQ-SQL :)). It also emits the javascript required for client side validation.
EDIT: Sorry left out the link for how to get it working for LINQ-SQL
The basic workflow goes something like this.
public partial class YourClass
{
[Required(ErrorMessage = "Property is required.")]
[StringLength(200)]
public string SomeProperty{ get; set; }
}
try
{
// Validate the instance of your object
var obj = new YourClass() { SomeProperty = "" }
var errors = DataAnnotationsValidationRunner.GetErrors(obj);
// Do some more stuff e.g. Insert into database
}
catch (RulesException ex)
{
// e.g. control name 'Prefix.Title'
ex.AddModelStateErrors(ModelState, "Prefix");
ModelState.SetModelValue("Prefix.Title", new ValueProviderResult(ValueProvider["Prefix.Title"].AttemptedValue, collection["Prefix.Title"], System.Globalization.CultureInfo.CurrentCulture));
}
how about this?
[RegularExpression(#"^[a-zA-Z''-'\s]{1,40}$",
ErrorMessage = "Characters are not allowed.")]
That should allow you to tag properties with specific error messages for whatever MVC validators you want to use...
In ASP.NET MVC 1, I met this problem too.
In my project, there is a model or business object named "Entry", and its primary key EntryId is int? type, and the value of EntryId can be allowd to input by users.
So the problem is, when the field is blank or zero or some integer value that has existed, the custom error messages can be shown well, but if the value is some non-integer value like "a", i can not find a way to use the custom message to replace the default message like "The value 'a' is invalid".
when i track the error message in ModelState, i found when the value is non-integer, there will be two errors related to EntryId, and the first item's error message is blank...
Now i have to use such an ugly code to hack the problem.
if (ModelState["EntryId"].Errors.Count > 1)
{
ModelState["EntryId"].Errors.Clear(); //should not use ModelState["EntryId"].remove();
ModelState.AddModelError("EntryId", "必须为大于0的整数"); //必须为大于0的整数 means "it should be an integer value and great than 0"
}
but this makes controller fat, hope there is a real solution to solve it.
Look up ModelState.AddError.
yes, there is a way, you must use System.ComponentModel.DataAnnotations in combination with xVal and you are going to be able to set validation rules and messages (u can even use resource files for localization) for each of your property using Attributes
look here http://blog.codeville.net/2009/01/10/xval-a-validation-framework-for-aspnet-mvc/