Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 852
Kobi Avatar asked Mar 05 '13 09:03

Kobi


2 Answers

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;
    }
}
like image 137
Tom Maher Avatar answered Oct 29 '22 22:10

Tom Maher


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; }
    }
like image 21
Kibria Avatar answered Oct 29 '22 23:10

Kibria