开发者

How do you get model metadata from inside a ValidationAttribute?

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):

priva开发者_开发技巧te 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.)


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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜