Reputation: 2014
I have created a custom attribute that implements several properties that will accept values for their usage as shown below.
public class CustomAttribute : OrderAttribute
{
public string Name { get; set; }
public bool Ignore { get; set; }
public override int Order { get; set; } = -1;
}
public abstract OrderAttribute : Attribute
{
public virtual int Order { get; set; }
}
After that, I have created class models to use the CustomAttribute
.
public abstract class Person
{
[Custom(Name = "Frist Name")]
public string FirstName { get; set; }
[Custom(Name = "Last Name")]
public string LastName { get; set; }
[Custom(Name = "Email")]
public string Email { get; set; }
}
public class Student : Person
{
[Custom(Name = "Address", Order = 2)]
public string Address { get; set; }
[Custom(Name = "Grade", Order = 5)]
public int Grade { get; set; }
}
Since I'm using this with the C# Reflection, I have to use the typeof
and get the class properties. This is when I have to sort the order of the properties based on their defined order using the OrderBy
.
typeof(Student).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.OrderBy(KeySelector)
.ToList();
Using this method in the OrderBy
.
private static int KeySelector(PropertyInfo prop)
{
var attr = prop.GetCustomAttribute<CustomAttribute>();
return attr?.Order ?? -1;
}
However, what it does is this:
What I want is for any property that has the [Custom(Name = "Test")]
and not implement the Order property is to be retained on its order or that has default order value of -1
. So the order should be something like:
Upvotes: 1
Views: 1351
Reputation: 5812
Here is my solution, it's a bit long winded because as mentioned in the comments the order of the result of GetProperties
is not guaranteed. With .NET 4.5+ there is a way around that as described by this answer.
So, the first part is to make sure we can get the properties of the class and its base class(es) in a predicatable order regardless of our custom order. To do that we recurse from the base class upwards and gather the properties that are declared in each, sorting the results by the order in which the property appears in the class.
public static IEnumerable<PropertyMetaData> GetPropertiesOrdered(Type someType, int inheritanceLevel = 1)
{
List<PropertyMetaData> seenProperties = new List<PropertyMetaData>();
if (someType.BaseType != (typeof(object)))
seenProperties.AddRange(GetPropertiesOrdered(someType.BaseType, inheritanceLevel + 1));
var properties = someType
.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
.Select(a => new PropertyMetaData(a, inheritanceLevel))
.Where(a => a.AttributeData != null);
properties = properties
.OrderBy(a => a.AttributeData.ClassOrder)
.Select((a, ordinal) =>
{
a.OrderWithinClass = ordinal + 1;
return a;
});
return seenProperties
.Union(properties)
.OrderByDescending(a => a.InheritanceLevel)
.ThenBy(a => a.OrderWithinClass)
.Select((a, ordinal) =>
{
a.OrderOverall = ordinal + 1;
return a;
});
}
To support that, the attribute changes slightly as below to use the compiler line number attribute as in the answered linked:
public class CustomAttribute : OrderAttribute
{
public CustomAttribute([CallerLineNumber]int order = 0) : base(order)
{
}
public string Name { get; set; }
public bool Ignore { get; set; }
public override int Order { get; set; } = -1;
}
public abstract class OrderAttribute : Attribute
{
private readonly int _classOrder;
public OrderAttribute([CallerLineNumber]int order = 0)
{
_classOrder = order;
}
public int ClassOrder { get { return _classOrder; } }
public virtual int Order { get; set; }
}
So, now we've got the existing properties in a predictable, numbered order, starting with the base class Person
properties then followed by the top level Student
class properties. The output of the GetPropertiesOrdered
method is a container class with information about the order of the properties encountered, the custom attribute associated with each, and code to handle the custom sort - which is, if an order is defined then prefer it to the order of the properties seen in the type.
public class PropertyMetaData : IComparable<PropertyMetaData>
{
public PropertyMetaData(PropertyInfo propertyInfo, int inheritanceLevel)
{
InheritanceLevel = inheritanceLevel;
PropertyInfo = propertyInfo;
AttributeData = propertyInfo.GetCustomAttribute<CustomAttribute>();
}
public int InheritanceLevel { get; set; }
public int OrderWithinClass { get; set; }
public int OrderOverall { get; set; }
public CustomAttribute AttributeData { get; set; }
public PropertyInfo PropertyInfo { get; set; }
public int GetOrder()
{
return HasCustomOrder() ? AttributeData.Order : this.OrderOverall;
}
public bool HasCustomOrder()
{
return AttributeData.Order != -1;
}
public int CompareTo(PropertyMetaData other)
{
var myOrder = GetOrder();
var otherOrder = other.GetOrder();
int compare = myOrder.CompareTo(otherOrder);
if (compare != 0 || other == this) return compare;
if (HasCustomOrder() && other.HasCustomOrder()) return 0;
if (HasCustomOrder() && !other.HasCustomOrder()) return -1;
return 1;
}
}
Putting it altogether it can be called like this:
var propertiesSorted =
GetPropertiesOrdered(typeof(Student))
.OrderBy(a => a);
Which gives us back the fields in the required order for your example: https://dotnetfiddle.net/dd5hVN
Upvotes: 1