简体   繁体   中英

MVC 3 custom DataAnnotation: associate error message with particular property

I've defined a custom DataAnnotation attribute similar to this one that goes on the class but ensures that at least one property is populated. It works correctly and adds an error message to the model's ValidationSummary . However, I want to be able to associate the error message with a particular property (or any string, really) so that I can display it in a particular place on my view.

Thus, if my custom attribute is used like this:

[RequireAtLeastOne(GroupId = 0, ErrorMessage = "You must specify at least one owner phone number.")]
public class UserViewModel: User {
    ...
}

then I want to be able to say something like:

[RequireAtLeastOne(GroupId = 0, ErrorMessage = "You must specify at least one owner phone number.", ValidationErrorKey = "my_key")]
public class UserViewModel: User {
    ...
}

...and use it in a view like this:

@Html.ValidationMessage("my_key")

It would also be fine if I had to associate the error message with a particular property on my model instead of an arbitrary string. How can I accomplish this?

Using ryudice's answer and this question as the starting point, I was able to solve this problem using IValidatableObject . For anyone interested, here is the full code I ended up with:

1. Define a custom validation attribute, RequireAtLeastOneAttribute

This attribute goes on the class to indicate that validation should check for property groups and ensure that at least one property from each group is populated. This attribute also defines the error message and an ErrorMessageKey , which will be used to keep track of the error messages and display them in the view instead of using the general-purpose ValidationSummary collection.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class RequireAtLeastOneAttribute: ValidationAttribute {

    /// <summary>
    /// This identifier is used to group properties together.
    /// Pick a number and assign it to each of the properties
    /// among which you wish to require one.
    /// </summary>
    public int GroupId { get; set; }

    /// <summary>
    /// This defines the message key any errors will be associated
    /// with, so that they can be accessed via the front end using
    /// @Html.ValidationMessage(errorMessageKey).
    /// </summary>
    public string ErrorMessageKey { get; set; }

    public override bool IsValid(object value) {
        // Find all properties on the class having a "PropertyGroupAttribute"
        // with GroupId matching the one on this attribute
        var typeInfo = value.GetType();
        var propInfo = typeInfo.GetProperties();
        foreach (var prop in propInfo) {
            foreach (PropertyGroupAttribute attr in prop.GetCustomAttributes(typeof(PropertyGroupAttribute), false)) {
                if (attr.GroupId == this.GroupId
                    && !string.IsNullOrWhiteSpace(prop.GetValue(value, null).GetString())) {
                    return true;
                }
            }
        }
        return false;
    }

}

2. Define a custom attribute PropertyGroupAttribute

This will be used to define which property groups need to have at least one value filled in.

[AttributeUsage(AttributeTargets.Property)]
public class PropertyGroupAttribute : Attribute {

    public PropertyGroupAttribute(int groupId) {
        this.GroupId = groupId;
    }

    public int GroupId { get; set; }

}

3. Attach the attributes to the model and properties

Group properties together using the "GroupId" integer (which can be anything, as long as it's the same for all properties among which at least one must be filled in).

[RequireAtLeastOne(GroupId = 0, ErrorMessage = "You must specify at least one owner phone number.", ErrorMessageKey = "OwnerPhone")]
[RequireAtLeastOne(GroupId = 1, ErrorMessage = "You must specify at least one authorized producer phone number.", ErrorMessageKey = "AgentPhone")]
public class User: IValidatableObject {

    #region Owner phone numbers
    // At least one is required

    [PropertyGroup(0)]
    public string OwnerBusinessPhone { get; set; }

    [PropertyGroup(0)]
    public string OwnerHomePhone { get; set; }

    [PropertyGroup(0)]
    public string OwnerMobilePhone { get; set; }

    #endregion

    #region Agent phone numbers
    // At least one is required

    [PropertyGroup(1)]
    public string AgentBusinessPhone { get; set; }

    [PropertyGroup(1)]
    public string AgentHomePhone { get; set; }

    [PropertyGroup(1)]
    public string AgentMobilePhone { get; set; }

    #endregion
}

4. Implement IValidatableObject on the model

public class User: IValidatableObject {

    ...

    #region IValidatableObject Members

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
        var results = new List<ValidationResult>();

        // This keeps track of whether each "RequireAtLeastOne" group has been satisfied
        var groupStatus = new Dictionary<int, bool>();
        // This stores the error messages for each group as defined
        // by the RequireAtLeastOneAttributes on the model
        var errorMessages = new Dictionary<int, ValidationResult>();
        // Find all "RequireAtLeastOne" property validators 
        foreach (RequireAtLeastOneAttribute attr in Attribute.GetCustomAttributes(this.GetType(), typeof(RequireAtLeastOneAttribute), true)) {
            groupStatus.Add(attr.GroupId, false);
            errorMessages[attr.GroupId] = new ValidationResult(attr.ErrorMessage, new string[] { attr.ErrorMessageKey });
        }

        // For each property on this class, check to see whether
        // it's got a PropertyGroup attribute, and if so, see if
        // it's been populated, and if so, mark that group as "satisfied".
        var propInfo = this.GetType().GetProperties();
        bool status;
        foreach (var prop in propInfo) {
            foreach (PropertyGroupAttribute attr in prop.GetCustomAttributes(typeof(PropertyGroupAttribute), false)) {
                if (groupStatus.TryGetValue(attr.GroupId, out status) && !status
                    && !string.IsNullOrWhiteSpace(prop.GetValue(this, null).GetString())) {
                    groupStatus[attr.GroupId] = true;
                }
            }
        }

        // If any groups did not have at least one property 
        // populated, add their error messages to the
        // validation result.
        foreach (var kv in groupStatus) {
            if (!kv.Value) {
                results.Add(errorMessages[kv.Key]);
            }
        }

        return results;
    }

    #endregion
}

5. Use the validation messages in the view

The validation messages will be saved as whatever ErrorMessageKey you specified in the RequireAtLeastOne attribute definition - in this example, OwnerPhone and AgentPhone .

@Html.ValidationMessage("OwnerPhone")

Caveats

The built-in validation also adds an error message to the ValidationSummary collection, but only for the first attribute defined on the model . So in this example, only the message for OwnerPhone would show up in ValidationSummary , since it was defined first on the model. I haven't looked for a way around this because in my case it didn't matter.

您可以在模型上实现IValidatableObject并在那里执行自定义逻辑,它将使您可以使用所需的任何键添加消息。

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM