Can I use an IMetadataAware attribute multiple times on the same field?
Asked Answered
L

2

7

I have fields that different people should see in different names.

For example, suppose I have the following user types:

public enum UserType {Expert, Normal, Guest}

I implemented an IMetadataAware attribute:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class DisplayForUserTypeAttribute : Attribute, IMetadataAware
{
    private readonly UserType _userType;

    public DisplayForUserTypeAttribute(UserType userType)
    {
        _userType = userType;
    }

    public string Name { get; set; }

    public void OnMetadataCreated(ModelMetadata metadata)
    {
        if (CurrentContext.UserType != _userType)
            return;
        metadata.DisplayName = Name;
    }
}

The idea is that I can override other values as needed, but fall back on default values when I don't. For example:

public class Model
{
    [Display(Name = "Age")]
    [DisplayForUserType(UserType.Guest, Name = "Age (in years, round down)")]
    public string Age { get; set; }

    [Display(Name = "Address")]
    [DisplayForUserType(UserType.Expert, Name = "ADR")]
    [DisplayForUserType(UserType.Normal, Name = "The Address")]
    [DisplayForUserType(UserType.Guest, Name = "This is an Address")]
    public string Address { get; set; }
}

The problem is that when I have multiple attributes of the same type, DataAnnotationsModelMetadataProvider only runs OnMetadataCreated for the first one.
In the example above, Address can only be shown as "Address" or "ADR" - the other attributes are never executed.

If I try to use different attributes - DisplayForUserType, DisplayForUserType2, DisplayForUserType3, everything is working as expected.

Am I doing anything wrong here?

Lindsylindy answered 5/3, 2013 at 9:48 Comment(0)
H
11

I know I am bit late to this party but I was looking to the answer to same question and couldn't find it anywhere on the web. In the end I worked it out myself.

The short answer is yes you can have multiple attributes of the same type that implement IMetadataAware interface on the same field / property. You just have to remember to override the TypeId of the Attribute class when extending it and replace it with something that will give you a unique object per instance of each derived attribute.

If you don't override the TypeId property of derived attribute then all the attributes of that type are treated the same since the default implementation returns the run time type of the attribute as the id.

So the following should now work as desired:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class DisplayForUserTypeAttribute : Attribute, IMetadataAware
{
    private readonly UserType _userType;

    public DisplayForUserType(UserType userType)
    {
        _userType = userType;
    }

    public override object TypeId
    {
        get
        {
            return this;
        }
    }

    public string Name { get; set; }

    public void OnMetadataCreated(ModelMetadata metadata)
    {
        if (CurrentContext.UserType != _userType)
            return;
        metadata.DisplayName = Name;
    }
}
Hydrophilous answered 6/3, 2014 at 15:49 Comment(3)
I didn't know about TypeId. Here's a good explanation if anyone is interested: Should the TypeIds of two attributes which are semantically identical be different or the same? Thanks a lot, and (a late) welcome to Stack Overflow!Lindsylindy
No worries, glad I could help - even if it was a year late :-)Hydrophilous
CurrentConte3xt is not available anymore. Is it possible to achieve something similar in MVC5 and higher?Diomedes
I
1

You implementation is not wrong, but any attribute that implements IMetadataAware is applied by the AssociatedMetadataProvider (and any derived type) after the Metadata creation. To override the default behavior, you may implement custom ModelMetadataProvider.

Here is another alternate quick solution:

Remove the interface IMetadataAware from the DisplayForUserType class.

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
    public class DisplayForUserTypeAttribute : Attribute//, IMetadataAware
    {
        //your existing code...
    }

Define a new IMetadataAware attribute that will apply the display logic by UserType, as below:

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class ApplyDisplayForUserTypeAttribute : Attribute, IMetadataAware
    {
        private readonly string _property;
        public ApplyDisplayForUserTypeAttribute(string property)
        {
            this._property = property;
        }

        public void OnMetadataCreated(ModelMetadata metadata)
        {
            var attribues = GetCustomAttributes(metadata.ContainerType
                                                        .GetProperty(this._property), typeof(DisplayForUserTypeAttribute))
                                                        .OfType<DisplayForUserTypeAttribute>().ToArray();
            foreach (var displayForUserTypeAttribute in attribues)
            {
                displayForUserTypeAttribute.OnMetadataCreated(metadata);
            }
        }
    }

And model will be:

public class Model
    {
        [Display(Name = "Age")]
        [DisplayForUserType(UserType.Guest, Name = "Age (in years, round down)")]
        [ApplyDisplayForUserType("Age")]
        public string Age { get; set; }

        [Display(Name = "Address")]
        [DisplayForUserType(UserType.Expert, Name = "ADR Expert")]
        [DisplayForUserType(UserType.Normal, Name = "The Address Normal")]
        [DisplayForUserType(UserType.Guest, Name = "This is an Address (Guest)")]
        [ApplyDisplayForUserType("Address")]
        public string Address { get; set; }
    }
Imbecility answered 5/3, 2013 at 17:48 Comment(2)
Thanks! Actually, I already have a quick fix - I defined DisplayForExpertUser, DisplayForNormalUser and DisplayForGuestUser - which is working pretty nicely.Lindsylindy
I don't really understand the first sentence: "any attribute that implements IMetadataAware is applied by the AssociatedMetadataProvider (and any derived type) after the Metadata creation" - if that were the case, it should have worked. The behavior I see here looks like a bug, but I don't want to jump to conclusions - are you suggesting there is a good explanation?Lindsylindy

© 2022 - 2024 — McMap. All rights reserved.