views:

543

answers:

1

MVC2 comes with a nice sample of a validation attribute called "PropertiesMustMatchAttribute" that will compare two fields to see if they match. Use of that attribute looks like this:

[PropertiesMustMatch("NewPassword", "ConfirmPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public class ChangePasswordModel
{
    public string NewPassword { get; set; }
    public string ConfirmPassword { get; set; }
}

The attribute is attached to the model class and uses a bit of reflection to do its work. You'll also notice the error message here is specified directly: "The new password and confirmation password do not match."

If you don't specify the message, the default message is generated using some code like this (shortened for clarity):

private const string _defaultErrorMessage = "'{0}' and '{1}' do not match.";
public override string FormatErrorMessage(string name)
{
    return String.Format(CultureInfo.CurrentUICulture, _defaultErrorMessage,
        OriginalProperty, ConfirmProperty);
}

The problem with that is that the "OriginalProperty" and "ConfirmProperty" are the hardcoded strings in the attribute - "NewPassword" and "ConfirmPassword" in this example. They don't actually get the real model metadata (e.g., the DisplayNameAttribute) to put together a more flexible, localizable message. I'd like to have a more generally applicable comparison attribute that uses the metadata display name info and so forth that's been specified.

Assuming I don't want to create a custom error message for every instance of my ValidationAttribute, that means I need to get a reference to the model metadata (or, at the very least, the model type that I'm validating) so I can get that metadata information and use it in my error messages.

How do I get a reference to the model metadata for the model I'm validating from inside the attribute?

(While I've found several questions that ask about how to validate dependent fields in a model, none of the answers include handling the error message properly.)

+3  A: 

This is actually a subset of the question of how to get the instance decorated by an attribute, from the attribute (similar question here).

Unfortunately, the short answer is: You can't.

Attributes are metadata. An attribute does not know and cannot know anything about the class or member it decorates. It is up to some downstream consumer of the class to look for said custom attributes and decide if/when/how to apply them.

You have to think of attributes as data, not objects. Although attributes are technically classes, they are rather dumb, because they have a crucial constraint: Everything about them must be defined at compile-time. This effectively means they can't get access to any runtime information, unless they expose a method that takes a runtime instance and the caller decides to invoke it.

You could do the latter. You could design your own attribute, and as long as you control the validator, you can make the validator invoke some method on the attribute and have it do pretty much anything:

public abstract class CustomValidationAttribute : Attribute
{
    // Returns the error message, if any
    public abstract string Validate(object instance);
}

As long as whomever uses this class uses the attribute properly, this will work:

public class MyValidator
{
    public IEnumerable<string> Validate(object instance)
    {
        if (instance == null)
            throw new ArgumentNullException("instance");
        Type t = instance.GetType();
        var validationAttributes = (CustomValidationAttribute[])Attribute
            .GetCustomAttributes(t, typeof(CustomValidationAttribute));
        foreach (var validationAttribute in validationAttributes)
        {
            string error = validationAttribute.Validate(instance);
            if (!string.IsNullOrEmpty(error))
                yield return error;
        }
    }
}

If this is how you consume the attributes, it becomes straightforward to implement your own:

public class PasswordValidationAttribute : CustomValidationAttribute
{
    public override string Validate(object instance)
    {
        ChangePasswordModel model = instance as ChangePasswordModel;
        if (model == null)
            return null;
        if (model.NewPassword != model.ConfirmPassword)
            return Resources.GetLocalized("PasswordsDoNotMatch");
        return null;
    }
}

This is all fine and good, it's just that the control flow is inverted from what you specify in the original question. The attribute doesn't know anything about what it was applied to; the validator that uses the attribute has to supply that information (which it can easily do).

Of course, this isn't how validation actually works with data annotations in MVC 2 (unless it's changed significantly since I last looked at it). I don't think you'll be able to just plug this in with ValidationMessageFor and other similar functions. But hey, in MVC 1, we had to write all our own validators anyway. There's nothing stopping you from combining DataAnnotations with your own custom validation attributes and validators, it's just going to involve a little more code. You'd have to invoke your special validator wherever you write validation code.

This is probably not the answer you're looking for, but it's the way it is, unfortunately; a validation attribute has no way to know about the class it was applied to unless that information is supplied by the validator itself.

Aaronaught
Not the answer I wanted, but most likely correct from what I've seen. It's unfortunate that MVC2 isn't [currently] using the ValidationContext overloads of the various methods because that could have been my way in. Most likely I will end up having to localize each individual message to avoid using the metadata or will have to hack something non-standard together. :(
Travis Illig
@Travis: This is one of the reasons I use FluentValidation instead of DataAnnotations. The rules are configured in actual code instead of attributes, so you can easily do things like set up custom rules and localize error messages. Might not be exactly what you're looking for either, but it's worth checking out. I think Microsoft modeled DataAnnotations after Castle ActiveRecord but left out a few important pieces.
Aaronaught
The answer for DataAnnotations is to go sort of unsupported. Following the MVC2 "PropertiesMustMatchAttribute" example, in the call to IsValid you get a reference to the model being validated. You can then call System.Web.Mvc.ModelMetadataProviders.Current.GetMetadataForProperties() to get the metadata for the properties on the model. From there you can get the display names for the properties you're validating and store them temporarily for use later in FormatErrorMessage. Not great, but works.In the end, though, you're right - technically you can't/shouldn't do what I'm trying to do.
Travis Illig