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; }
}
}
}