ewahner
ewahner

Reputation: 1189

Getting the DisplayNameAttribute from an Internal class

I have a class that is declared Internal. It is decorated with various annotations. In particular is the [DisplayName("My Display Name")] annotation. I have some code that will retrieve the value but only works if the class is declared public. I am sort of new to using reflection. I believe I need to specify that the BindingFlags.NonPublic be used but I am not sure where.

LinqPAD code:

void Main()
{
    List<SpGetProfileInfoResult> p = new List<SpGetProfileInfoResult>();
    p.Add(new SpGetProfileInfoResult() { FName = "Eric" });
    p.Add(new SpGetProfileInfoResult() { FName = "Mike" });

    p.Dump();

    foreach (var item in p)
    {
        Console.WriteLine(item.DisplayName(i => i.FName));
        Console.WriteLine(item.FName);
    }

}

public partial class SpGetProfileInfoResult
{
    // Uncomment this annotation to see that this part will work
    // [System.ComponentModel.DisplayNameAttribute("[BILLTO-FNAME]")]
    public string FName { get; set; }
}

public partial class SpGetProfileInfoResult
{
    internal class Metadata
    {
        // This attribute is never available seems.
        [System.ComponentModel.DisplayNameAttribute("[BILL-FNAME]")]
        public string FName { get; set; }
    }
}

public static class Tag
{
    public static T GetAttribute<T>(this MemberInfo member, bool isRequired) where T : Attribute
    {
        var attribute = member.GetCustomAttributes(typeof(T), false).SingleOrDefault();

        if (attribute == null && isRequired)
        {
            throw new ArgumentException(
                string.Format(
                "The {0} attribute must be defined on member {1}",
                typeof(T).Name,
                member.Name));
        }

        return (T)attribute;
    }

    public static string DisplayName<T>(this T src,Expression<Func<T, object>> propertyExpression)
    {
        Type metadata = null;

        var memberInfo = GetPropertyInformation(propertyExpression.Body);
        if (memberInfo == null)
        {
            throw new ArgumentException(
                "No property reference expression was found.",
                "propertyExpression");
        }

        var attr = memberInfo.GetAttribute<DisplayNameAttribute>(false);
        if (attr == null)
        {
            return memberInfo.Name;
        }

        return attr.DisplayName;
    }

    public static MemberInfo GetPropertyInformation(Expression propertyExpression)
    {
        MemberExpression memberExpr = propertyExpression as MemberExpression;
        if (memberExpr == null)
        {
            UnaryExpression unaryExpr = propertyExpression as UnaryExpression;
            if (unaryExpr != null && unaryExpr.NodeType == ExpressionType.Convert)
            {
                memberExpr = unaryExpr.Operand as MemberExpression;
            }
        }

        if (memberExpr != null && memberExpr.Member.MemberType == MemberTypes.Property)
        {
            return memberExpr.Member;
        }

        return null;
    }
}

Usage:

If you don't have LinqPAD, you should download it then you can test this pretty easily by just creating a new C# Program in LinkPAD

Debug.WriteLine(item.DisplayName(i => i.FName));

Upvotes: 2

Views: 2304

Answers (3)

ewahner
ewahner

Reputation: 1189

After working on this for a while I came up with a Hack. I am sure someone out there can help me clean this up a bit, but this is what I found works. I had to add the "Metadata" nested class to the DeclaringType and then do a GetMember on that result, which returns a collection of members.

public static string DisplayName<T>(this T src, Expression<Func<T, object>> propertyExpression)
{
    var memberInfo = GetPropertyInformation(propertyExpression.Body);
    var mytype = src.GetType();
    string strType = mytype.Name + "+Metadata";
    var metaType = Type.GetType(strType);
    MemberInfo[] mem = metaType.GetMember(memberInfo.Name);
    var att = mem[0].GetCustomAttributes(typeof(DisplayNameAttribute), true).FirstOrDefault() as DisplayNameAttribute;

    if (att == null)
        return memberInfo.Name;
    else
        return att.DisplayName;
}

Upvotes: 1

AakashM
AakashM

Reputation: 63338

So it looks like you want to be able to decorate existing members of a partial class, by providing metadata in a separate partial piece. There's no built-in mechanism for that (see eg this question and the classes mentioned in the answer), but if you're willing to stick to a convention, you can roll your own:

So suppose we have

public partial class SpGetProfileInfoResult
{
    public string FName { get; set; }
}

in a partial piece we can't change, and

public partial class SpGetProfileInfoResult
{
    internal class Metadata
    {
        [System.ComponentModel.DisplayNameAttribute("[BILL-FNAME]")]
        public string FName { get; set; }
    }
}

in a partial piece we can change. You already have most of the pieces: in DisplayName(), you successfully determine that we are looking at the FName property; you then look for a DisplayNameAttribute on T.FName, but there isn't one, so that's where it stops.

What you need to do is, in the case where you don't find the attribute you need,

var attr = memberInfo.GetAttribute<DisplayNameAttribute>(false);
if (attr == null)
{

Look for a nested class named Metadata - note here is one place we use BindingFlags.NonPublic

    // Try and get a nested metadata class
    var metadataType = typeof(T)
        .GetNestedType("Metadata", 
                       BindingFlags.Public | BindingFlags.NonPublic);

If we find one:

    if (metadataType != null)
    {

Look for a member of the same name as was originally being talked about (BindingFlags.NonPublic again)

        var membersOnMetadataType = metadataType.GetMember(memberInfo.Name, 
            BindingFlags.Instance |
            BindingFlags.Public | 
            BindingFlags.NonPublic);

If there is one, use your helper method, but this time pass it the metadata type's member:

        if (membersOnMetadataType.Any())
        {
            var attrOnMetadataType = membersOnMetadataType[0]
                .GetAttribute<DisplayNameAttribute>(false);
            return attrOnMetadataType.DisplayName;

(I've omitted a final nullity check here, as well as closing the control flow)

Depending on how distasteful you find that "Metadata" string, you could instead do something declarative with attributes:

  • have a class-level attribute that goes on SpGetProfileInfoResult (the piece you can change) that points at its Metadata using typeof (this is the approach taken by System.ComponentModel), or
  • have a class-level attribute that goes on Metadata, to have it claim 'I am a metadata type'. Then instead of searching for a nested class named a fixed string, we would instead search for a nested class having this particular attribute.

Upvotes: 3

Igby Largeman
Igby Largeman

Reputation: 16747

I won't try to debug your code because you're using some classes that I'm not familiar with.

One thing I do know is that MemberInfo does not have a GetAttribute() function. You must be using an extension method there.

However I can tell you that you don't need any special bindingflags just because the type is internal. Only the visibility of the member is important, and in this case it's public.

using System;
using System.ComponentModel;

namespace ConsoleApplication1
{
    internal class Metadata
    {
        [DisplayName("[BILL-FNAME]")]
        public string FName { get; set; }
    } 

    class Program
    {
        static void Main()
        {
            var memberInfo = typeof(Metadata).GetMember("FName")[0];
            var atrributes = memberInfo.GetCustomAttributes(false);
            Console.WriteLine(atrributes[0].GetType().Name);
        }
    }
}

Output:

DisplayNameAttribute

Upvotes: 0

Related Questions