Guarantee order of properties in a list matches the order they appear in code file
I have an interface that defines a method for returning an IList<PropertyInfo>
:
public interface IWriteable
{
IList<PropertyInfo> WriteableProperties();
}
.
. It is implemented in various (dissimilar) classes in the following manner:public abstract class Foo
{
private IList<PropertyInfo> _props;
protected Foo()
{
this._props = new List<PropertyInfo>();
foreach (PropertyInfo p in this.GetType().GetProperties())
{
if (Attribute.IsDefined(p, typeof(WriteableAttribute)))
this._props.Add(p);
}
}
#region IWriteable Members
public IList<PropertyInfo> WriteableProperties()
{
return this._props;
}
#endregion
}
public class Bar : Foo
{
public string A
{
get { return "A"; }
}
[Writeable()]
public string B
{
get { return "B"; }
}
[Writeable()]
public string C
{
get { return "C"; }
}
// Snip
}
Please note the attributes marking a couple of the properties, as these are the properties that will get added to the list. This IList
will then be used elsewhere during some file write operations.
It is important to me that they are ordered in the list in the order they appear in the code file.
However, MSDN states:
The GetProperties method does not return properties in a particular order, such as alphabetical or declaration order. Your code must not depend on the order in which properties are returned, because that order varies.
So, what is the best way to ensure each PropertyInfo gets added in the order I would like to to be?
(I 开发者_开发技巧am also using .NET2.0, so I can't use any Linq goodness, should there be any that would help, although it would be interesting to see.)
Add information to the attribute about the ordering, you can then use this to ensure the ordering, e.g.:
[Writeable(Order = 1)]
So for the following attribute:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class WriteableAttribute : Attribute
{
public int Order { get; set; }
}
You can get an ordered selection of the properties as follows:
private readonly List<PropertyInfo> _props;
protected Foo()
{
_props = new List<PropertyInfo>();
var props = new Dictionary<int, PropertyInfo>();
foreach (PropertyInfo p in GetType().GetProperties())
{
if (Attribute.IsDefined(p, typeof(WriteableAttribute)))
{
var attr = (WriteableAttribute)p
.GetCustomAttributes(typeof(WriteableAttribute))[0];
props.Add(attr.Order, p);
}
}
_props.AddRange(props.OrderBy(kvp => kvp.Key).Select(kvp => kvp.Value));
}
NB For production code I would recommend caching the property information (per type for example) as this will be relatively slow if carried out for each instance.
Update - Caching
With some example caching of property lookup and ordering:
public static class PropertyReflector
{
private static readonly object SyncObj = new object();
private static readonly Dictionary<Type, List<PropertyInfo>> PropLookup =
new Dictionary<Type, List<PropertyInfo>>();
public static IList<PropertyInfo> GetWritableProperties(Type type)
{
lock (SyncObj)
{
List<PropertyInfo> props;
if (!PropLookup.TryGetValue(type, out props))
{
var propsOrder = new Dictionary<int, PropertyInfo>();
foreach (PropertyInfo p in type.GetProperties())
{
if (Attribute.IsDefined(p, typeof(WriteableAttribute)))
{
var attr = (WriteableAttribute)p.GetCustomAttributes(
typeof(WriteableAttribute), inherit: true)[0];
propsOrder.Add(attr.Order, p);
}
}
props = new List<PropertyInfo>(propsOrder
.OrderBy(kvp => kvp.Key)
.Select(kvp => kvp.Value));
PropLookup.Add(type, props);
}
return props;
}
}
}
Update - No Linq
You can replace the Linq section with the following code to order the properties and add them to the cache:
List<int> order = new List<int>(propsOrder.Keys);
order.Sort();
props = new List<PropertyInfo>();
order.ForEach(i => props.Add(propsOrder[i]));
PropLookup.Add(type, props);
Update - Full Linq
And using a fully Linq solution:
static IList<PropertyInfo> GetWritableProperties(Type type)
{
lock (SyncObj)
{
List<PropertyInfo> props;
if (!PropLookup.TryGetValue(type, out props))
{
props = type.GetProperties()
.Select(p => new { p, Atts = p.GetCustomAttributes(typeof(WriteableAttribute), inherit: true) })
.Where(p => p.Atts.Length != 0)
.OrderBy(p => ((WriteableAttribute)p.Atts[0]).Order)
.Select(p => p.p)
.ToList();
PropLookup.Add(type, props);
}
return props;
}
}
A while ago when I had the same problem I wrote a helper class to sort the properties based on the Order
property of the attribute. I used the built-in DisplayAttribute
but you can just add an Order
property to any attribute you write.
class FieldSorter : IComparer, IComparer<DisplayAttribute>, IEqualityComparer<DisplayAttribute>
{
public int Compare(object x, object y)
{
return Compare((DisplayAttribute)x, (DisplayAttribute)y);
}
public int Compare(DisplayAttribute x, DisplayAttribute y)
{
return x.Order.CompareTo(y.Order);
}
public bool Equals(DisplayAttribute x, DisplayAttribute y)
{
return Compare(x, y) == 0;
}
public int GetHashCode(DisplayAttribute obj)
{
return obj.GetHashCode();
}
public static SortedList<DisplayAttribute, PropertyInfo> GetSortedFields(Type type)
{
PropertyInfo[] props = type.GetProperties();
var sortedProps = new SortedList<DisplayAttribute, PropertyInfo>(props.Length, new FieldSorter());
object[] atts;
int assignedOrder = 1000; // anything without pre-assigned order gets a ridiculously high order value. same for duplicates.
foreach (var prop in props)
{
atts = prop.GetCustomAttributes(typeof(DisplayAttribute), true);
if (atts.Length > 0)
{
var att = (DisplayAttribute)atts[0];
if (!att.GetOrder().HasValue || sortedProps.Keys.Contains(att, new FieldSorter()))
att.Order = assignedOrder++;
sortedProps.Add(att, prop);
}
}
return sortedProps;
}
}
This gives you a SortedList
where the key is the attribute and the value is the PropertyInfo. This was because I still needed to access other properties of the attribute.
Example usage:
public class Stats
{
[Display(Name = "Changes", Description = "Changed records.", Order = 8)]
public int RecordsWithChanges { get; set; }
[Display(Name = "Invalid", Description = "Number of invalid records analyzed.", Order = 4)]
public int InvalidRecordCount { get; set; }
[Display(Name = "Valid", Description = "Number of valid records.", Order = 6)]
public int ValidRecordCount { get; set; }
[Display(Name = "Cost", Description = "Number of records with a Cost value.", Order = 10)]
public int RecordsWithCost { get; set; }
public Stats(int changed, int valid, int invalid, int cost)
{
RecordsWithChanges = changed;
ValidRecordCount = valid;
InvalidRecordCount = invalid;
RecordsWithCost = cost;
}
}
class Program
{
static void Main(string[] args)
{
var foo = new Stats(123, 456, 7, 89);
var fields = FieldSorter.GetSortedFields(foo.GetType());
foreach (DisplayAttribute att in fields.Keys)
Console.WriteLine("{0}: {1} ({2}) == {3}",
att.Order, att.Name, att.Description, fields[att].GetValue(foo, null));
null));
}
}
Output:
4: Invalid (Number of invalid records analyzed.) -- 7 6: Valid (Number of valid records.) -- 456 8: Changes (Changed records.) -- 123 10: Cost (Number of records with a Cost value.) -- 89
精彩评论