using System; using System.Collections.Generic; using System.Reflection; using System.Text; namespace Com.GuyMahieu.Util { /// /// Helps to get and set property values on objects through reflection. /// Properties of underlying objects can be accessed directly by separating /// the levels in the hierarchy by dots. /// To get/set the name of an Ancestor, for objects that have a Parent property, /// you could use "Parent.Parent.Parent.Name". /// public class PropertyReflector { private const char PropertyNameSeparator = '.'; private static readonly object[] NoParams = new object[0]; private static readonly Type[] NoTypeParams = new Type[0]; private IDictionary propertyCache = new Dictionary(); private IDictionary constructorCache = new Dictionary(); /// /// Gets the Type of the given property of the given targetType. /// The targetType and propertyName parameters can't be null. /// /// the target type which contains the property /// the property to get, can be a property on a nested object (eg. "Child.Name") public Type GetType(Type targetType, string propertyName) { if (propertyName.IndexOf(PropertyNameSeparator) > -1) { string[] propertyList = propertyName.Split(PropertyNameSeparator); for (int i = 0; i < propertyList.Length; i++) { string currentProperty = propertyList[i]; targetType = GetTypeImpl(targetType, currentProperty); } return targetType; } else { return GetTypeImpl(targetType, propertyName); } } /// /// Gets the value of the given property of the given target. /// If objects within the property hierarchy are null references, null will be returned. /// The target and propertyName parameters can't be null. /// /// the target object to get the value from /// the property to get, can be a property on a nested object (eg. "Child.Name") public object GetValue(object target, string propertyName) { if (propertyName.IndexOf(PropertyNameSeparator) > -1) { string[] propertyList = propertyName.Split(PropertyNameSeparator); for (int i = 0; i < propertyList.Length; i++) { string currentProperty = propertyList[i]; target = GetValueImpl(target, currentProperty); if (target == null) { return null; } } return target; } else { return GetValueImpl(target, propertyName); } } /// /// Sets the value of the given property on the given target to the given value. /// If objects within the property hierarchy are null references, an attempt will be /// made to construct a new instance through a parameterless constructor. /// The target and propertyName parameters can't be null. /// /// the target object to set the value on /// the property to set, can be a property on a nested object (eg. "Child.Name") /// the new value of the property public void SetValue(object target, string propertyName, object value) { if (propertyName.IndexOf(PropertyNameSeparator) > -1) { object originalTarget = target; string[] propertyList = propertyName.Split(PropertyNameSeparator); for (int i = 0; i < propertyList.Length - 1; i++) { propertyName = propertyList[i]; target = GetValueImpl(target, propertyName); if (target == null) { string currentFullPropertyNameString = GetPropertyNameString(propertyList, i); target = Construct(GetType(originalTarget.GetType(), currentFullPropertyNameString)); SetValue(originalTarget, currentFullPropertyNameString, target); } } propertyName = propertyList[propertyList.Length - 1]; } SetValueImpl(target, propertyName, value); } /// /// Returns a string containing the properties in the propertyList up to the given /// level, separated by dots. /// For the propertyList { "Zero", "One", "Two" } and level 1, the string /// "Zero.One" will be returned. /// /// the array containing the properties in the corect order /// the level up to wich to include the properties in the returned string /// a dot-separated string containing the properties up to the given level private static string GetPropertyNameString(string[] propertyList, int level) { StringBuilder currentFullPropertyName = new StringBuilder(); for (int j = 0; j <= level; j++) { if (j > 0) { currentFullPropertyName.Append(PropertyNameSeparator); } currentFullPropertyName.Append(propertyList[j]); } return currentFullPropertyName.ToString(); } /// /// Returns the type of the given property on the target instance. /// The type and propertyName parameters can't be null. /// /// the type of the target instance /// the property to retrieve the type for /// the typr of the given property on the target type private Type GetTypeImpl(Type targetType, string propertyName) { return GetPropertyInfo(targetType, propertyName).PropertyType; } /// /// Returns the value of the given property on the target instance. /// The target instance and propertyName parameters can't be null. /// /// the instance on which to get the value /// the property for which to get the value /// the value of the given property on the target instance private object GetValueImpl(object target, string propertyName) { return GetPropertyInfo(target.GetType(), propertyName).GetValue(target, NoParams); } /// /// Sets the given property of the target instance to the given value. /// Type mismatches in the parameters of these methods will result in an exception. /// Also, the target instance and propertyName parameters can't be null. /// /// the instance to set the value on /// the property to set the value on /// the value to set on the target private void SetValueImpl(object target, string propertyName, object value) { GetPropertyInfo(target.GetType(), propertyName).SetValue(target, value, NoParams); } /// /// Obtains the PropertyInfo for the given propertyName of the given type from the cache. /// If it is not already in the cache, the PropertyInfo will be looked up and added to /// the cache. /// /// the type to resolve the property on /// the name of the property to return the PropertyInfo for /// private PropertyInfo GetPropertyInfo(Type type, string propertyName) { PropertyInfoCache propertyInfoCache = GetPropertyInfoCache(type); if (!propertyInfoCache.ContainsKey(propertyName)) { PropertyInfo propertyInfo = GetBestMatchingProperty(propertyName, type); if (propertyInfo == null) { throw new ArgumentException(string.Format("Unable to find public property named {0} on type {1}", propertyName, type.FullName), propertyName); } propertyInfoCache.Add(propertyName, propertyInfo); } return propertyInfoCache[propertyName]; } /// /// Gets the best matching property info for the given name on the given type if the same property is defined on /// multiple levels in the object hierarchy. /// private static PropertyInfo GetBestMatchingProperty(string propertyName, Type type) { PropertyInfo[] propertyInfos = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy); PropertyInfo bestMatch = null; int bestMatchDistance = int.MaxValue; for (int i = 0; i < propertyInfos.Length; i++) { PropertyInfo info = propertyInfos[i]; if (info.Name == propertyName) { int distance = CalculateDistance(type, info.DeclaringType); if (distance == 0) { // as close as we're gonna get... return info; } if (distance > 0 && distance < bestMatchDistance) { bestMatch = info; bestMatchDistance = distance; } } } return bestMatch; } /// /// Calculates the hierarchy levels between two classes. /// If the targetObjectType is the same as the baseType, the returned distance will be 0. /// If the two types do not belong to the same hierarchy, -1 will be returned. /// private static int CalculateDistance(Type targetObjectType, Type baseType) { if (!baseType.IsInterface) { Type currType = targetObjectType; int level = 0; while (currType != null) { if (baseType == currType) { return level; } currType = currType.BaseType; level++; } } return -1; } /// /// Returns the PropertyInfoCache for the given type. /// If there isn't one available already, a new one will be created. /// /// the type to retrieve the PropertyInfoCache for /// the PropertyInfoCache for the given type private PropertyInfoCache GetPropertyInfoCache(Type type) { if (!propertyCache.ContainsKey(type)) { lock(this) { if (!propertyCache.ContainsKey(type)) { propertyCache.Add(type, new PropertyInfoCache()); } } } return propertyCache[type]; } /// /// Creates a new object of the given type, provided that the type has a default (parameterless) /// constructor. If it does not have such a constructor, an exception will be thrown. /// /// the type of the object to construct /// a new instance of the given type private object Construct(Type type) { if (!constructorCache.ContainsKey(type)) { lock (this) { if (!constructorCache.ContainsKey(type)) { ConstructorInfo constructorInfo = type.GetConstructor(NoTypeParams); if (constructorInfo == null) { throw new Exception(string.Format("Unable to construct instance, no parameterless constructor found in type {0}", type.FullName)); } constructorCache.Add(type, constructorInfo); } } } return constructorCache[type].Invoke(NoParams); } } /// /// Keeps a mapping between a string and a PropertyInfo instance. /// Simply wraps an IDictionary and exposes the relevant operations. /// Putting all this in a separate class makes the calling code more /// readable. /// internal class PropertyInfoCache { private IDictionary propertyInfoCache; public PropertyInfoCache() { propertyInfoCache = new Dictionary(); } public bool ContainsKey(string key) { return propertyInfoCache.ContainsKey(key); } public void Add(string key, PropertyInfo value) { propertyInfoCache.Add(key, value); } public PropertyInfo this[string key] { get { return propertyInfoCache[key]; } set { propertyInfoCache[key] = value; } } } }