Setting property by name with automatic type conversion

I liked the idea of binding to properties in WPF or Workflows. So implemented something similar (although less powerful) in my code: I had a list of tasks and their data context, so I needed somehow to “bind” data from context to properties of tasks. As a part of implementation, I have created a method that allowed me to take an object (task), take one of its properties by name and assign that property a value. Everything in reflection, since property name nor its value is not known at design time and only evaluated at runtime.

Remembering that my value is always a string, I had to implement some sort of automatic conversion to target type from string. Later I have extended the method to accept any type, not just string.

Implementation was split into two different methods, as I needed type conversion as a standalone code elsewhere. Here are two methods:

 /// <summary>
 /// Fail-safe method that converts data into another type. See Remarks for more information.
 /// </summary>
 /// <param name="data">Data to convert</param>
 /// <param name="targetType">Target type. Must not be <c>null</c></param>
 /// <param name="newValue">New value</param>
 /// <returns><c>true</c> if data was converted successfully, <c>false</c> otherwise</returns>
 /// <exception cref="ArgumentNullException"><c>targetType</c> is <c>null</c> which is not expected</exception>
 /// <exception cref="InvalidCastException">When failed to convert data to specified type</exception>
 /// <remarks>
 /// This method tries to convert data of one type to another by performing following operations in specified
 /// order:
 /// <list type="bullet">
 ///     <item>
 ///         <description>If <paramref name="data"/> is <c>null</c>, result is also <c>null</c>.</description>
 ///     </item>
 ///     <item>
 ///         <description>If <paramref name="targetType"/> is assignable from type of <paramref name="data"/>, result is the same as data.</description>
 ///     </item>
 ///     <item>
 ///         <description>
 ///             If <paramref name="targetType"/> is enumeration and type of <paramref name="data"/> is string,
 ///             enumeration parsing is made using <see cref="Enum.Parse"/> method.
 ///         </description>
 ///     </item>
 ///     <item>
 ///         <description>
 ///             If all previous conditions not met, type is converted to another type by calling <see cref="Convert.ChangeType"/> method.
 ///         </description>
 ///     </item>
 ///     <item>
 ///         <description>
 ///             If conversion fails with <see cref="InvalidCastException"/> exception, <paramref name="targetType"/> in
 ///             inspected for presence of constuctor that accepts parameter of type of <paramref name="data"/>. In case
 ///             such constructor is found, it is called and its result is returned. Otherwise exception is rethrown.
 ///         </description>
 ///     </item>
 /// </list>
 /// </remarks>
 public static bool TryConvertData(object data, Type targetType, out object newValue)
 {
     newValue = null;
  
     if (targetType == null)
         throw new ArgumentNullException("targetType");
  
     bool ret = false;
     try
     {
         if (data == null)
         {
             newValue = null;
         }
         else if (targetType.IsAssignableFrom(data.GetType()))
         {
             newValue = data;
         }
         else if (targetType.IsEnum && data is string)
         {
             newValue = Enum.Parse(targetType, data.ToString(), true);
         }
         else
         {
             var converter = TypeDescriptor.GetConverter(targetType);
             if (converter.CanConvertFrom(data.GetType()))
                 newValue = converter.ConvertFrom(data);
             else
             {
                 //failed to convert data, let's see if we can find a suitable constructor - our last resort
                 ConstructorInfo ctor = targetType.GetConstructor(new Type[] { data.GetType() });
                 if (ctor != null)
                     newValue = ctor.Invoke(new object[] { data });
                 else
                     return false;
             }
         }
         ret = true;
     }
     catch { }
     return ret;
 }
  
 /// <summary>
 /// Sets a valud of public, non-static property or field for given object, converting data to target type
 /// as needed.
 /// </summary>
 /// <param name="target">Object instance to set property or field value on.</param>
 /// <param name="propertyName">Name of public, non-static property or field</param>
 /// <param name="value">Value for property</param>
 /// <param name="throwOnError">Whether throw when error occurs or continue.</param>
 /// <exception cref="ArgumentNullException">When <c>target</c> is <c>null</c>. This exception is thrown regardless of throwOnError parameter value.</exception>
 /// <exception cref="InvalidCastException">When value cannot be converted to target type. Thrown only when <b>throwOnError</b> is <c>true</c></exception>
 /// <exception cref="InvalidOperationException">When no property or field found with specified name. Thrown only when <b>throwOnError</b> is <c>true</c></exception>
 public static void SetPropertyValue(object target, string propertyName, object value, bool throwOnError)
 {
     if (target == null)
     {
         throw new ArgumentNullException("target");
     }
  
     Type type = target.GetType();
  
     //check if property with such name exists in target type
     Type memberType = null;
     PropertyInfo propertyInfo = type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public);
     if (propertyInfo != null && propertyInfo.CanWrite)
         memberType = propertyInfo.PropertyType;
  
     //if no such property, we check for public field
     if (memberType == null)
     {
         FieldInfo fieldInfo = type.GetField(propertyName, BindingFlags.Instance | BindingFlags.Public);
         if (fieldInfo != null)
             memberType = fieldInfo.FieldType;
     }
  
     //if we couldn't find exact match, maybe try case-insensitive?
     if (memberType == null)
     {
         propertyInfo = type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);
         if (propertyInfo != null && propertyInfo.CanWrite)
         {
             memberType = propertyInfo.PropertyType;
             propertyName = propertyInfo.Name;
         }
  
         //if no such property, we check for public field
         if (memberType == null)
         {
             FieldInfo fieldInfo = type.GetField(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);
             if (fieldInfo != null)
             {
                 memberType = fieldInfo.FieldType;
                 propertyName = fieldInfo.Name;
             }
         }
     }
  
     if (memberType != null)
     {
         object targetValue = null;
         if (TryConvertData(value, memberType, out targetValue))
         {
             type.InvokeMember(propertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty | BindingFlags.SetField, null,
                 target, new object[] { targetValue });
         }
         else if (throwOnError)
         {
             throw new InvalidCastException(String.Format("Cannot convert from '{0}' to '{1}'", value.GetType().FullName, memberType.FullName));
         }
     }
     else if (throwOnError)
     {
         throw new InvalidOperationException(String.Format("Property or field '{0}' not found on type '{1}'", propertyName, type.FullName));
     }
 }
  
Technorati Tags: ,

posted @ Wednesday, July 22, 2009 11:12 AM

Print

Comments on this entry:

# re: Setting property by name with automatic type conversion

Left by web development company at 8/28/2009 3:22 PM
Gravatar
Nice post,

Thanks

# re: Setting property by name with automatic type conversion

Left by RAF at 11/16/2009 7:45 PM
Gravatar
Thanks for sharing, but I have a classical copy problem again:
how can I copy this code properly?
All is on 1 line when I paste and neither can I benefit from autoindent because the numbers are preceding, and I'm too old to have patience to do this manually one more time.

# @RAF

Left by Patrick at 11/16/2009 9:30 PM
Gravatar
I have removed all numbers from post, should be find copy-pasting it. Thanks for noting :)

# re: Setting property by name with automatic type conversion

Left by Jobair at 11/18/2009 11:14 AM
Gravatar
Very helpful post

# re: Setting property by name with automatic type conversion

Left by Target at 1/21/2010 11:50 AM
Gravatar
Nice post,

Thanks

Your comment:



 (will not be displayed)


 
 
 
Please add 4 and 5 and type the answer here:
 

Live Comment Preview: