Jim McCurdy's Tech Blog

Insights into Software Development and Silverlight

Generic class for deep clone of Silverlight and CLR objects

Update: As of 8/11/2010, I have released the following update of the source code for my CloneObject class.  This update adds the ability to clone any attached properties found in the loaded assemblies, and also removes an unnecessary dependency property loop which will speed up some operations…

Update: As of 7/19/2010, I have updated the source code for my CloneObject class.  The update improves on the ability to clone arrays and other IEnumerable objects, and simplifies some other operations…

The hardest part of developing this CloneObject class, was testing all of the potential use cases.  The solution I offer here works well for the common use cases that I was able to test, but I am sure that others may find ways to make it more robust.  Readers are encouraged to comment with modifications.  Thanks to Justin Angel and Tamir Khason for their inspirational articles.  My solution took a slightly different direction over time, but they were the ones that got me started.

The code below has been tested and used with Silverlight 3 and Silverlight 4.

//#define DEBUG_TRACE
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Resources;

namespace ClassLibrary
{
	//[Obfuscation(Exclude = true)]
	internal static class CloneObject
	{
		private static List<FieldInfo> _attachedProperties = null;

		// Extension for any class object
		internal static TT DeepClone<TT>(this TT source, bool? cloneAttachedProperties = null)
		{ // Jim McCurdy's DeepClone
			if (cloneAttachedProperties == null)
				cloneAttachedProperties = (source is DependencyObject);

			// The first time this method is called, compute a list of all
			// attached properties that exist in this XAP's assemblies
			if (cloneAttachedProperties == true && _attachedProperties == null)
			{
				_attachedProperties = new List<FieldInfo>();
				List<Assembly> assemblies = GetLoadedAssemblies();
				foreach (Assembly assembly in assemblies)
					GetAttachedProperties(_attachedProperties, assembly);
			}

			TT clone = CloneRecursive(source);

			if (clone is FrameworkElement)
			{
				FrameworkElement cloneElement = (clone as FrameworkElement);
				cloneElement.Arrange(new Rect(0, 0, cloneElement.ActualWidth, cloneElement.ActualHeight));
			}

			return clone;
		}

		private static TT CloneRecursive<TT>(TT source)
		{
			if (source == null || source.GetType().IsValueType)
				return source;

			// Common types that do not have parameterless constructors
			if (source is string || source is Type || source is Uri || source is DependencyProperty)
				return source;

			TT clone = CloneCreate(source);
			if (clone == null)
				return source;

			if (source is IList)
				CloneList(source as IList, clone as IList);

			CloneProperties(source, clone);

			return clone;
		}

		private static TT CloneCreate<TT>(TT source)
		{
			try
			{
#if DEBUG_TRACE
				string.Format("Clone create object Type={0}", SimpleType(source.GetType())).Trace();
#endif
				Array sourceArray = (source as Array);
				if (sourceArray == null)
					return (TT)Activator.CreateInstance(source.GetType());
				if (sourceArray.Rank == 1)
					return (TT)(object)Array.CreateInstance(source.GetType().GetElementType(),
						sourceArray.GetLength(0));
				if (sourceArray.Rank == 2)
					return (TT)(object)Array.CreateInstance(source.GetType().GetElementType(),
						sourceArray.GetLength(0), sourceArray.GetLength(1));
			}
			catch (Exception ex)
			{
				if (ex.Message.Contains("No parameterless constructor"))
					return default(TT);
				string.Format("Can't create object Type={0}", SimpleType(source.GetType())).Trace();
				ex.DebugOutput();
			}

			return default(TT);
		}

		private static void CloneProperties(object source, object clone)
		{
			// The binding flags indicate what properties we will clone
			// Unfortunately, we cannot clone "internal" or "protected" properties
			BindingFlags flags = 
				BindingFlags.Instance | BindingFlags.FlattenHierarchy | BindingFlags.Public;

			if (source is DependencyObject)
			{
				DependencyObject sourcedp = source as DependencyObject;
				DependencyObject clonedp = clone as DependencyObject;

				// Clone attached properties
				if (_attachedProperties != null && _attachedProperties.Count > 0)
					foreach (FieldInfo field in _attachedProperties)
						CloneDependencyProperty(sourcedp, clonedp, field, true);

				// Clone dependency properties
				FieldInfo[] fields = source.GetType().GetFields(flags | BindingFlags.Static);
				foreach (FieldInfo field in fields)
					CloneDependencyProperty(sourcedp, clonedp, field, false);
			}

			// Clone CLR properties
			PropertyInfo[] properties = source.GetType().GetProperties(flags);
			foreach (PropertyInfo property in properties)
				CloneProperty(source, clone, property);
		}

		private static void CloneDependencyProperty(DependencyObject sourceObject, 
			DependencyObject cloneObject, FieldInfo field, bool isAttached)
		{
			try
			{
				// Blacklisted properties that can't (or shouldn't) be set
				if (field.Name == "NameProperty" && sourceObject is FrameworkElement) return;

				DependencyProperty dp = field.GetValue(sourceObject) as DependencyProperty;
				if (dp == null) // Event DependencyProperties will be null
					return;

				object sourceValue = null;
				try
				{
					sourceValue = sourceObject.GetValue(dp);
				}
				catch (Exception)
				{
				}

				if (sourceValue == null)
					return;
				// Don't set attached properties if we don't have to
				if (isAttached)
				{
					Type sourceType = sourceValue.GetType();
					if (sourceType.IsValueType && sourceValue.Equals(Activator.CreateInstance(sourceType)))
						return;
				}
#if DEBUG_TRACE
				string.Format("Clone dependency property Name={0}, Value={2} for source Type={1}", 
					field.Name, SimpleType(sourceObject.GetType()), sourceValue).Trace();
#endif
				// Blacklisted properties that can't (or don't need to be) cloned
				bool doClone = true;
				if (field.Name == "DataContextProperty") doClone = false;
				//if (field.Name == "TargetPropertyProperty") doClone = false;

				object cloneValue = (doClone ? CloneRecursive(sourceValue) : sourceValue);
				cloneObject.SetValue(dp, cloneValue);
			}
			catch (Exception ex)
			{
				if (ex.Message.Contains("read-only"))
					return;
				if (ex.Message.Contains("read only"))
					return;
				if (ex.Message.Contains("does not fall within the expected range"))
					return;
				string.Format("Can't clone dependency property Name={0}, for source Type={1}", 
					field.Name, SimpleType(sourceObject.GetType())).Trace();
				ex.DebugOutput();
			}
		}

		private static void CloneProperty(object source, object clone, PropertyInfo property)
		{
			try
			{
				if (!property.CanRead || !property.CanWrite || property.GetIndexParameters().Length != 0)
					return;

				// Blacklisted properties that can't (or shouldn't) be set
				if (property.Name == "Name" && source is FrameworkElement) return;
				if (property.Name == "InputScope" && source is TextBox) return; // Can't get
				if (property.Name == "Watermark" && source is TextBox) return; // Can't get
				if (property.Name == "Source" && source is ResourceDictionary) return; // Can't set
				if (property.Name == "TargetType" && source is ControlTemplate) return; // Can't set

				bool publicSetter = (source.GetType().GetMethod("set_" + property.Name) != null);
				bool isList = (property.PropertyType.GetInterface("IList", true) != null);
				if (!publicSetter && !isList)
					return;

				object sourceValue = property.GetValue(source, null);
				if (sourceValue == null)
					return;

				if (!publicSetter && isList)
				{
					IList cloneList = property.GetValue(clone, null) as IList;
					if (cloneList != null)
						CloneList(sourceValue as IList, cloneList);
					return;
				}
#if DEBUG_TRACE
				string.Format("Clone property Type={0}, Name={1}, Value={3} for source Type={2}", 
					SimpleType(property.PropertyType), property.Name, SimpleType(source.GetType()), 
					sourceValue).Trace();
#endif
				// Blacklisted properties that can't (or don't need to be) cloned
				bool doClone = true;
				if (source is FrameworkElement && property.Name == "DataContext") doClone = false;
				//if (property.Name == "TargetProperty") doClone = false;

				object cloneValue = (doClone ? CloneRecursive(sourceValue) : sourceValue);
				property.SetValue(clone, cloneValue, null); // possible MethodAccessException
			}
			catch (Exception ex)
			{
				string.Format("Can't clone property Type={0}, Name={1}, for source Type={2}", 
					SimpleType(property.PropertyType), property.Name, SimpleType(source.GetType())).Trace();
				ex.DebugOutput();
			}
		}

		private static void CloneList(IList sourceList, IList cloneList)
		{
			try
			{
				IEnumerator sourceEnumerator = sourceList.GetEnumerator();
				Array sourceArray = sourceList as Array;
				Array cloneArray = cloneList as Array;
				int dim0 = (sourceArray != null && sourceArray.Rank > 0 ? sourceArray.GetLowerBound(0) : 0);
				int dim1 = (sourceArray != null && sourceArray.Rank > 1 ? sourceArray.GetLowerBound(1) : 0);

				while (sourceEnumerator.MoveNext())
				{
					object sourceValue = sourceEnumerator.Current;
#if DEBUG_TRACE
					string.Format("Clone IList item {0}", sourceValue).Trace();
#endif
					object cloneValue = CloneRecursive(sourceValue);
					if (sourceArray == null)
						cloneList.Add(cloneValue);
					else
					if (sourceArray.Rank == 1)
						cloneArray.SetValue(cloneValue, dim0++);
					else
					if (sourceArray.Rank == 2)
					{
						cloneArray.SetValue(cloneValue, dim0, dim1);
						if (++dim1 > sourceArray.GetUpperBound(1))
						{
							dim1 = sourceArray.GetLowerBound(1);
							if (++dim0 > sourceArray.GetUpperBound(0))
								dim0 = sourceArray.GetLowerBound(0);
						}
					}
				}
			}
			catch (Exception ex)
			{
				string.Format("Can't clone IList item Type={0}", SimpleType(sourceList.GetType())).Trace();
				ex.DebugOutput();
			}
		}

		private static string SimpleType(Type type)
		{
			string typeName = type.ToString();
			int index = typeName.LastIndexOf('[');
			if (index < 0)
				return typeName.Substring(typeName.LastIndexOf('.') + 1);

			string collectionName = typeName.Substring(index);
			collectionName = collectionName.Substring(collectionName.LastIndexOf('.') + 1);
			typeName = typeName.Substring(0, index);
			typeName = typeName.Substring(typeName.LastIndexOf('.') + 1);
			return typeName + '[' + collectionName;
		}

		private static List<Assembly> GetLoadedAssemblies()
		{
			List<Assembly> assemblies = new List<Assembly>();

			foreach (AssemblyPart part in Deployment.Current.Parts)
			{
				StreamResourceInfo sri = 
					Application.GetResourceStream(new Uri(part.Source, UriKind.Relative));
				if (sri == null)
					continue;
				Assembly assembly = new AssemblyPart().Load(sri.Stream);
				if (assembly != null && !assemblies.Contains(assembly))
					assemblies.Add(assembly);
			}

			// Additional assemblies that are not found when examining of Deployment.Current.Parts above
			Type[] types =
			{
				typeof(System.Windows.Application), // System.Windows.dll,
				#if INCLUDE_ASSEMBLIES_WITHOUT_ATTACHED_PROPERTIES
				typeof(System.Action), // mscorlib.dll,
				typeof(System.Uri), // System.dll,
				typeof(System.Lazy<int>), // System.Core.dll,
				typeof(System.Net.Cookie), // System.Net.dll,
				typeof(System.Runtime.Serialization.StreamingContext), // System.Runtime.Serialization.dll,
				typeof(System.ServiceModel.XmlSerializerFormatAttribute), // System.ServiceModel.dll,
				typeof(System.Windows.Browser.BrowserInformation), // System.Windows.Browser.dll,
				typeof(System.Xml.ConformanceLevel), // System.Xml.dll,
				#endif
			};

			foreach (Type type in types)
			{
				Assembly assembly = type.Assembly;
				if (assembly != null && !assemblies.Contains(assembly))
					assemblies.Add(assembly);
			}

			return assemblies;
		}

		private static bool GetAttachedProperties(List<FieldInfo> attachedProperties, Assembly assembly)
		{
			BindingFlags flags = BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Static;
			foreach (Type type in assembly.GetTypes())
			{
				FieldInfo[] fields = type.GetFields(flags);
				MethodInfo[] methods = null;
				foreach (FieldInfo field in fields)
				{
					if (!field.FieldType.Is(typeof(DependencyProperty)))
						continue;
					if (!field.Name.EndsWith("Property"))
						continue;

					string fieldName = field.Name.Replace("Property", "");
					string getName = "Get" + fieldName;
					string setName = "Set" + fieldName;
					bool foundGet = false;
					bool foundSet = false;
					if (methods == null)
						methods = type.GetMethods(flags);
					foreach (MethodInfo method in methods)
					{
						if (method.Name == getName && method.GetParameters().Length == 1 && 
							method.GetParameters()[0].ParameterType.Is(typeof(DependencyObject)))
							foundGet = true;
						else
						if (method.Name == setName && method.GetParameters().Length == 2 && 
							method.GetParameters()[0].ParameterType.Is(typeof(DependencyObject)))
							foundSet = true;
						if (foundGet && foundSet)
							break;
					}

					if (!(foundGet && foundSet))
						continue;

					try
					{
						DependencyProperty dp = field.GetValue(null) as DependencyProperty;
					}
					catch (Exception)
					{
						continue;
					}

					// Found an attached Dependency Property
					attachedProperties.Add(field);
				}
			}

			return true;
		}
	}

	internal static class Extensions
	{
		// Extension for Type
		internal static bool Is(this Type type, Type baseType)
		{
			return (type.Equals(baseType) || type.IsSubclassOf(baseType));
		}

		// Extension for Exception
		internal static void DebugOutput(this Exception ex)
		{
#if DEBUG
			Debug.WriteLine(ex.GetType().ToString() + ": " + ex.Message + Environment.NewLine);
#endif
		}

		// Extension for string
		internal static void Trace(this string message)
		{
			Debug.WriteLine(message);
		}
	}
}

Comments (24) -

  • Joseph Gershgorin

    4/5/2010 9:20:42 AM |

    Thanks for posting this! Will this work for Storyboard objects? I've been looking for a Silverlight equivalent to WPF's Storyboard.Clone for ages. Also note, the above code is non functional without re-factoring unless you provide the definitions of your DebugOutput() and Trace() extension methods.

    • jim mccurdy

      5/17/2010 12:22:10 PM |

      Joe,

      This method should work with Storyboards, but let me know if you have issues.

      And thanks for info on the DebugOutput() and Trace() extension methods.  I added those methods in the code sample above.

      Jim

  • Kevin

    4/29/2010 5:44:50 PM |

    Should create extension method for each if the types to make it a little bit faster and cleaner

    • Jim McCurdy

      4/29/2010 6:13:36 PM |

      I do use extension methods: Clone<TT>() is an extension method for every object type.   Could you be more specific?

  • Fred

    6/5/2010 12:20:04 PM |

    Jim,
    I use clone() to rename elements when printing different kind of elements over several print pages, but I have an issue with grid's. I made a long SL page with several grid's with 4 rows by 4 columns, filled them with TextBlock's and formatted with borders. I'm also using (Grid.ColumnSpanProperty, 2) to place text and borders like headings.
    When printed the cell's in the grid are all printed on top of each other. Any idea how to solve this?

    • jim mccurdy

      6/5/2010 2:39:27 PM |

      It sounds like the "attached properties" like Grid.Row and Grid.Column are not being cloned.  I'll have to take a look when I get a chance.

      • gautham

        6/14/2010 3:15:41 AM |

        hello,

        How to make use of this class where having parameter along with the constructor.

        With Regards,
        Gautham

        • jim mccurdy

          6/14/2010 8:45:58 AM |

          Gautham,  objects need to have a parameter-less constructor in order to be generically cloned.

  • Steve

    7/1/2010 9:53:44 AM |

    Tried it to clone a stack panel containing hyperlink buttons.  It cloned the stack panel but not the hyperlink buttons contained in the panel.  thanks for the post. it is well written code.

    • jim mccurdy

      8/3/2010 4:10:10 PM |

      Steve,

      I made some updates to the code that should correct that issue.

  • Anand Subramanian

    9/14/2010 3:36:46 PM |

    Works like a charm, first time over....Thanks a lot.

    Anand

  • Ben

    11/11/2010 9:05:53 AM |

    SL 4 complains :
    >> bool? cloneAttachedProperties = null

    Error  1  Default parameter specifiers are not permitted  C:\dev\Clone.cs  19  82  Differ.Utilities

  • Marc Roussel

    11/15/2010 5:57:04 AM |

    Thank you for this great DeepClone.

    Is it me that doesn't know how it works or it's kind of slow and take a few seconds before the clone is done.

  • Marc Roussel

    11/15/2010 6:14:48 AM |

    What is the parameter cloneAttachedProperties which if I set to false it's very fase and if I do not set anything it's very slow.  I just don't know what it does and WHEN should I use it.

    • Marc Roussel

      11/15/2010 6:15:41 AM |

      fase=fast

      • jim mccurdy

        11/15/2010 10:24:20 AM |

        Marc,

        In August, I made a set of updates to DeepClone to include the ability to clone attached properties.  I made this feature optional.  If you are unfamiliar with attached properties, they are properties like Grid.Row, whereby a the static property of one class can be "attached" to any other class object.  In order to clone them, a much more extensive process must be undertaken, because DeepCLone has to search all of the loaded assemblies, since any class object could contain any other class's attached property.  

        I would only turn on cloneAttachedProperties in thoses cases whre you really need it, because the process of finding attached properties is time consuming as you point out.

        Jim

  • Scott

    11/18/2010 3:31:18 PM |

    Hello Jim,

    Nice work, I actually implemented this into a drag-drop scenario myself - maybe it was my VM but a "simple" UserControl took a few real seconds to Clone - are you finding some performance issues? Interested.

    Thanks again

    • jim mccurdy

      11/18/2010 7:34:35 PM |

      Scott,

      Be sure to set DeepClone()'s cloneAttachedProperties parameter to false.  That will save you a lot of time.  I need to change the code to set cloneAttachedProperties to false by default.

      Jim

  • Hugo Estrada

    12/1/2010 6:50:52 AM |

    Thanks so much! You have saved me a lot of time. Worked at the first try.

    • a game

      4/7/2012 10:38:16 AM |

      Asegúrese de ajustar DeepClone () 's cloneAttachedProperties parámetro en false. Esto le ahorrará mucho tiempo. Tengo que cambiar el código para establecer cloneAttachedProperties false de forma predeterminada.
      http://www.agame.fm

  • Raffles

    12/12/2010 7:36:29 PM |

    Hello Jim,

    Your class for clone object really work great. Just 1 problem when i used for object that have DataBinding to others object or self. If DataBinding to self will cause an infinite loop. And, binded to others object will cause a inconsistent data binding.

    Thank You.

    • Jim McCurdy

      12/12/2010 8:17:16 PM |

      Good catch.  I already special case the DataContext property in CloneDependencyProperty(), but maybe it needs to be treated like the NameProperty and just return null.

  • Dhanalakshmi

    11/4/2011 2:47:16 AM |

    Hello Jim,
       I am trying to print the UI Elements,so i am using this clone object method above code.But I got blank page only.

    Thanks,

  • jim mccurdy

    11/4/2011 9:37:11 AM |

    Is your issue a browser issue?

Comments are closed