Ryan Rivest

  Home :: Contact :: Syndication  :: Login
  8 Posts :: 0 Stories :: 10 Comments :: 0 Trackbacks

Archives

Post Categories

There are quite a few examples of using the attributes from System.ComponentModel.DataAnnotations out there in your ASP.NET MVC applications.  If you haven’t been keeping up with what’s new in ASP.NET MVC 2, one of the biggest improvements is in the area of validation – which is now automatic if you decorate your models with the attributes.  I recommend you check out ScottGu’s latest epic post on the topic to get up to speed.

Continuing with the Gu’s example, here is our Person class and the applied validation attributes:

    public class Person {
        [Required(ErrorMessage = "First Name is required.")]
        [StringLength(50, ErrorMessage = "First Name must be under 50 characters.")]
        public string FirstName { get; set; }

        [Required(ErrorMessage = "Last Name is required.")]
        [StringLength(50, ErrorMessage = "Last Name must be under 50 characters.")]
        public string LastName { get; set; }

        [Required(ErrorMessage = "Age is required.")]
        [Range(0, 120, ErrorMessage = "Age must be between 0 and 120.")]
        public int Age { get; set; }

        [Required(ErrorMessage = "Email is required.")]
        [Email(ErrorMessage = "You did not enter a valid email.")]
        public string Email { get; set; }
    }

This works great, but we can take it one step further and eliminate all the explicit error message strings by using a resource file.  To add one, hit Ctrl+Shift+A to launch the Add New Item dialog.  You can enter any name you like, I choose to keep it simple and named it Resources.

AddResourceFile

 

Next, we’ll add our error message strings to the resource file.  Make note of the format items ({0}, {1}, etc.), as this is how the property names and validation attribute parameters are added to the error message.  I’ll explain how this works later in this post.  It’s also important to make sure the Access Modifier is set to Public.  If it’s set to Internal, ASP.NET MVC 2’s DataAnnotations Model Binder won’t validate the property at all.  This took me a few minutes to figure out what was happening, so don’t make the same mistake I did.

ValidationErrorStrings

Now that we have our Resource file created and the validation error messages defined, we can change our Person class to make use of them.  To get this working, all you need to do is specify the ErrorMessageResourceName and ErrorMessageResourceType when applying a validation attribute.  Here’s our new Person class making use of the resources.

    public class Person {
        [Required(ErrorMessageResourceName = "Required_ValidationError", ErrorMessageResourceType = typeof(Resources))]
        [StringLength(50, ErrorMessageResourceName = "Required_ValidationError", ErrorMessageResourceType = typeof(Resources))]
        public string FirstName { get; set; }

        [Required(ErrorMessageResourceName = "Required_ValidationError", ErrorMessageResourceType = typeof(Resources))]
        [StringLength(50, ErrorMessageResourceName = "StringLength_ValidationError", ErrorMessageResourceType = typeof(Resources))]
        public string LastName { get; set; }

        [Required(ErrorMessageResourceName = "Required_ValidationError", ErrorMessageResourceType = typeof(Resources))]
        [Range(0, 120, ErrorMessageResourceName = "Range_ValidationError", ErrorMessageResourceType = typeof(Resources))]
        public int Age { get; set; }

        [Required(ErrorMessageResourceName = "Required_ValidationError", ErrorMessageResourceType = typeof(Resources))]
        [Email(ErrorMessageResourceName = "Email_ValidationError", ErrorMessageResourceType = typeof(Resources))]
        public string Email { get; set; }
    }

When we test the validation, we should see that the validation still works, and it’s now using our generalized error messages, substituting the property name and, if the validation attribute has any, the attribute parameters (as in the case of StringLength or the Range attributes).

ValidationTest

It works great now, but that code still looks a little verbose to me.  I don’t want to specify the resource string name and resource type every time I use one of the attributes.  My preferred approach is to subclass the validation attributes with my own of the same name.  Let’s do that now.  The strategy here is to pass the heavy lifting off to the original attribute’s constructor (if necessary), and initialize the 2 properties that we were hard-coding before.

    public class RequiredAttribute : System.ComponentModel.DataAnnotations.RequiredAttribute {
        public RequiredAttribute() {
            ErrorMessageResourceName = "Required_ValidationError";
            ErrorMessageResourceType = typeof (Resources);
        }
    }

    public class StringLengthAttribute : System.ComponentModel.DataAnnotations.StringLengthAttribute {
        public StringLengthAttribute(int maximumLength) : base(maximumLength) {
            ErrorMessageResourceName = "StringLength_ValidationError";
            ErrorMessageResourceType = typeof (Resources);
        }
    }

    public class RangeAttribute : System.ComponentModel.DataAnnotations.RangeAttribute {
        public RangeAttribute(int minimum, int maximum) : base(minimum, maximum) {
            InitializeErrorMessageResource();
        }

        public RangeAttribute(double minimum, double maximum) : base(minimum, maximum) {
            InitializeErrorMessageResource();
        }

        public RangeAttribute(Type type, string minimum, string maximum) : base(type, minimum, maximum) {
            InitializeErrorMessageResource();
        }

        private void InitializeErrorMessageResource() {
            ErrorMessageResourceName = "Range_ValidationError";
            ErrorMessageResourceType = typeof(Resources);
        }
    }

    public class EmailAttribute : RegularExpressionAttribute {
        public EmailAttribute() : base("^[a-z0-9_\\+-]+(\\.[a-z0-9_\\+-]+)*@[a-z0-9-]+(\\.[a-z0-9-]+)*\\.([a-z]{2,4})$") {
            ErrorMessageResourceName = "Email_ValidationError";
            ErrorMessageResourceType = typeof (Resources);
        }
    }

This really cleans up our model class, which now looks like this:

    public class Person {
        [Required]
        [StringLength(50)]
        public string FirstName { get; set; }

        [Required]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Range(0, 120)]
        public int Age { get; set; }

        [Required]
        [Email]
        public string Email { get; set; }
    }

Much better, and our validation still works as we expected, without all the string and type duplication.  The only problem I see now is that our error messages use the exact property name, which isn’t always what we want.  For example, when the user submits the form without entering a value in the FirstName field, they receive the error message “FirstName is required.”  I would much rather see “First Name is required.” instead.  Good news!  They made this really easy to change for nitpickers like me.

To get our property name showing up nicely, all we have to do is apply the DisplayName attribute to the FirstName property.

    [DisplayName("First Name")]
    public string FirstName { get; set; }

This also has the added benefit of updating the FirstName field’s label as well, since we’re using ASP.NET MVC 2’s strongly typed label HTML helper (which uses the property name if no display name is available).

DisplayNameError 

How it Works:  FormatErrorMessage()

Let’s take a peek inside the FormatErrorMessage() method on the StringLengthAttribute:

public override string FormatErrorMessage(string name) {
    return string.Format(CultureInfo.CurrentCulture, base.ErrorMessageString, new object[] { name, this.MaximumLength });
}

Every ValidationAttribute will either inherit this method, or implement their own (if it has additional parameters, like String Length, which has a maximum length that is supplied to the attribute.  As you can see, this method is simply using string.Format, and supplying the name as {0} and the MaximumLength property as {1}.  Simple enough, right?

Update:  As Kenneth and Rafael pointed out in the comments, the original sample code did not work with client validation. 

I could have sworn that client validation worked on these samples previously, but it's quite possible that I forgot to test it.

At any rate, the reason that it doesn't work when you subclass the attributes is because there are no client validation rules attached to the attribute anymore since it is a “new” validation attribute that MVC doesn't know about.

There are two things that you can do to get it working:

1. Use the more verbose method of defining the resource name and type with the default Data Annotations attributes.

2. Register the custom subclass adapters with their respective adapter type.  To do this, just add these lines to your Global.asax like this:

protected void Application_Start() {
    AreaRegistration.RegisterAllAreas(); 

    RegisterRoutes(RouteTable.Routes); 

    DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredAttribute), typeof(System.Web.Mvc.RequiredAttributeAdapter));
    DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(StringLengthAttribute), typeof(System.Web.Mvc.StringLengthAttributeAdapter));
    DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(EmailAttribute), typeof(System.Web.Mvc.RegularExpressionAttributeAdapter));
    DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RangeAttribute), typeof(System.Web.Mvc.RangeAttributeAdapter));
}

By the way, if you create any custom validation attributes that are not based on one of the existing attributes that would have an adapter already created, you will need to create an adapter class to set up the client validation rules and register it here as well.  Phil Haack has a great post outlining all of this information.

http://haacked.com/archive/2009/11/19/aspnetmvc2-custom-validation.aspx

If you want to poke around in the code, you can download the sample here.

I hope this helps..

posted on Friday, January 15, 2010 11:00 PM

Feedback

# re: Reusable Validation Error Message Resource Strings for DataAnnotations 4/13/2010 5:31 AM Kenneth Johansen
Hi Ryan, I like your implementation. I tried to use some of these attributes in my projects and they work, except that the client side validation is not included.

I also downloaded your sample and could not see that the clientside validation was working when I tested it on my local machine. Are you sure it works?

Regards Kenneth


# re: Reusable Validation Error Message Resource Strings for DataAnnotations 4/16/2010 4:13 PM Rafael
Hi Ryan,

I'm with the same problem as Kenneth.

When I subclass the RequiredAttribute, client side validation doesnt work anymore, do you have any idea why?

Best Regards,
Rafael

# re: Reusable Validation Error Message Resource Strings for DataAnnotations 4/16/2010 9:52 PM Ryan Rivest
Kenneth and Rafael,

Thanks for the comments - I somehow missed that client side validation wasn't working with this code.

Please look at my updated comments above and let me know how this works out for you :)

I also updated the sample code to include these changes.

Ryan

# re: Reusable Validation Error Message Resource Strings for DataAnnotations 4/17/2010 7:18 AM Rafael
Yeah, its worked.

Thanks Ryan.

# re: Reusable Validation Error Message Resource Strings for DataAnnotations 4/28/2010 6:39 AM Kenneth
Tested and it works like a charm, thanks Ryan :)

# re: Reusable Validation Error Message Resource Strings for DataAnnotations 5/25/2010 12:07 AM Susanne
Nice post! But I wonder how I replace the error message "Value '' is not valid for". In your example it should appear when you pass a string in your Age field. Do I have to make a custom validator for this?

# re: Reusable Validation Error Message Resource Strings for DataAnnotations 5/25/2010 10:37 AM Ryan Rivest
Hi Susanne,

This post by Brad Wilson on the ASP.NET MVC team details how you can use a custom message for that error.

http://forums.asp.net/p/1512140/3606268.aspx

Basically, you're going to need to create a global resource file (this part is important, I tried with the normal resource file I'm using in this example, but it doesn't work).

You want to use the key "PropertyValueInvalid" for the message, and then you can use something like "'{0}' is not valid for {1}".

The last thing you have to do is set the DefaultModelBinder.ResourceClassKey property to the name of your resource class (as a string).

# re: Reusable Validation Error Message Resource Strings for DataAnnotations 5/26/2010 12:24 AM Susanne
Thank you! I found it after som research yesterday and it works perfectly!

# re: Reusable Validation Error Message Resource Strings for DataAnnotations 5/26/2010 4:33 AM Kevin
It is working perfectly. But can i ask why the resource file need to embed? It make the application need to recompile whenever there is changes on that resource file. Is that anyway to make it as content like other global resource file?


# re: Reusable Validation Error Message Resource Strings for DataAnnotations 8/28/2010 6:05 AM Adri
Thanks , but now I want to use this with a ADO.NET EF4 within a partial class.
I don't get it to work.

[MetadataType(typeof(PersonMetaData))]
public partial class Person
{
[MetadataType(typeof(PersonMetaData))]
public class PersonMetaData
{
[Required]
public string FirstName{ get; set; }
}
}

I only get it working if I use this

[Required(ErrorMessageResourceName = "Required_ValidationError", ErrorMessageResourceType = typeof(Resources))]

Maybe you can help me? Thanks

Comments have been closed on this topic.