Contents
In this article, I will present a simple way to serialize business objects that are derived from System.Windows.DependencyObject
and use System.Windows.DependencyProperty
with the help of some reflection.
A project I am working on relies on DependencyProperties
in its data representation. This is a disputable design decision, since it makes the data dependent from a specific presentation layer. On the other hand, it prevents you from re-inventing the wheel. This discussion would lead too far, however, and is outside the scope of this article.
.NET Serialization is quite powerful out of the box. Why? Simply because the BinaryFormatter
is capable of serializing and deserializing object graphs automatically, thus saving you from creating unique IDs for objects, finding cyclic links, handling generic lists, deserializing in the correct order, etc. These cumbersome problems have been solved for us already, so it'd be great if we could harness these capabilities, again saving our time.
Please note, however, that not all formatters can do that. For example, the SoapFormatter
cannot handle generic lists. The BinaryFormatter
is certainly the most feature-complete formatter available out-of-the-box as of now, so we are going to use that in here.
In this example, we'll work with a tree structure, because it beautifully shows the power the BinaryFormatter
has. Note that the nodes have cyclic links because children know their parent node and vice versa. Also note the use of generic lists:
public class GraphNodeBase : DependencyObject
{
#region Graph Node Members
protected List mChildren;
protected GraphNodeBase mParent;
#endregion
public GraphNodeBase(GraphNodeBase _parent)
{
}
public void AddChild(GraphNodeBase newChild)
{
}
}
public class GraphNode : GraphNodeBase
{
#region Constructors
public GraphNode(GraphNodeBase _parent)
: base(_parent)
{
}
#endregion
#region Dependency Properties
public static DependencyProperty NameProperty =
DependencyProperty.Register("Name",
typeof(string),
typeof(GraphNode),
new PropertyMetadata(String.Empty));
public string Name
{
get { return (string)GetValue(NameProperty); }
set { SetValue(NameProperty, value); }
}
#endregion
public string AutoProperty
{
get;
set;
}
public int SomeReadOnlyProperty
{
get { return mSomeReadOnlyMember; }
}
}
The code is split into two classes to make the problem more realistic, since we will need to traverse the inheritance tree using reflection. More on that later. Note the read-only CLR property and the automatic property. The full source code contains a GraphNode
with some more members to give a better overview.
Now, what I'd like to see is basically the very same code for serialization and deserialization that is supplied in MSDN's most simple example, something like:
void SaveGraph(Stream stream)
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, MyGraphRootNode);
}
If the GraphNode
's name was not a DependencyProperty
, the code above would (almost) work. However, since GraphNodeBase
is derived from DependencyObject
, serialization will fail at run-time, complaining that class DependencyObject
is not marked as serializable. Now, we're stuck since we can't use DependencyProperty
if we don't derive from DependencyObject
. What now?
Now, unfortunately, while you can simply prevent an aggregated object from being taken into account during serialization using the [NonSerialized]
attribute, you cannot simply prevent the base class from being serialized unless you implement some steps of serialization manually using the ISerializable
interface in the derived class. So let's do that, and we end up with something like:
public class GraphNode : GraphNodeBase
{
protected GraphNode(SerializationInfo info,
StreamingContext context)
: base(info, context)
{
this.Name = info.GetString("Name");
}
public override void GetObjectData(SerializationInfo info,
StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue("Name", Name);
}
}
At first glance, this is an acceptable solution, but it forces us to write lots and lots of stupid and therefore error-prone code: In every class, we need to override GetObjectData()
and add the corresponding information, not forgetting to call the base class (which will result in a mess)! Oh, and we just killed the famous [NonSerialized]
attribute. Also, we need to provide a deserialization constructor. Failure to provide the latter results in a run-time error upon deserialization, which makes matters worse.
This simple example might not be too convincing, but for a complex object with lots of members this is very unnerving. Not to mention the additional overhead when modifying the class! Reflection comes to assist us here! Using reflection, we can assemble a list of members we want to serialize for each type:
public static class ReflectionHelper
{
private static Hashtable serializationLists = new Hashtable();
public static List<FieldInfo> GetSerializableFieldInformation(Type _type)
{
if (serializationLists.ContainsKey(_type))
{
return serializationLists[_type] as List<FieldInfo>;
}
if (_type.IsSubclassOf(typeof(DependencyObject)))
{
List<FieldInfo> fieldInformation = new List<FieldInfo>();
Type typeRover = _type;
while (typeRover != null && typeRover.BaseType != typeof(DependencyObject))
{
FieldInfo[] fields = typeRover.GetFields
(BindingFlags.Instance | BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
foreach (FieldInfo fiRover in fields)
{
if (fiRover.IsNotSerialized)
continue;
if (fiRover.FieldType.IsSubclassOf(typeof(MulticastDelegate)) ||
fiRover.FieldType.IsSubclassOf(typeof(Delegate)) )
continue;
fieldInformation.Add(fiRover);
}
typeRover = typeRover.BaseType;
}
serializationLists.Add(_type, fieldInformation);
return fieldInformation;
}
return null;
}
}
We tell the GetFields()
method to supply us with all instance members, whether they are public
or not (unlike the XamlWriter
which handles public
members only), but only those items declared in the respective class (using the DeclaredOnly
flag). For performance reasons, we cache our results in a Hashtable
. The code above will stop when reaching the level of SerializableDependencyObject
, so we don't drill down to the Object
class. Also note that we have to apply some tweaks here and there. For example, it is a very bad idea to serialize events, so we'll have to take that into account.
With this code, we can resemble the behaviour of default serialization by simply implementing GetObjectData()
in our base class and serializing all attributes of the object that are not marked with the NonSerializedAttribute [NonSerialized]
. Let's call this class SerializableDependencyObject
and insert it between DependencyObject
and GraphNodeBase
:
[Serializable]
public class SerializableDependencyObject : DependencyObject, ISerializable
{
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
PropertyDescriptorCollection descriptors = TypeDescriptor.GetProperties(GetType(),
new Attribute[] { new PropertyFilterAttribute
(PropertyFilterOptions.SetValues |
PropertyFilterOptions.UnsetValues |
PropertyFilterOptions.Valid ) } );
List<FieldInfo> fieldInformation =
ReflectionHelper.GetSerializableFieldInformation(GetType());
foreach (FieldInfo fiRover in fieldInformation)
{
info.AddValue(fiRover.Name, fiRover.GetValue(this));
}
foreach (PropertyDescriptor propertyDescriptor in descriptors)
{
if (!IsSerializableDependencyProperty(propertyDescriptor))
continue;
info.AddValue(propertyDescriptor.Name, propertyDescriptor.GetValue(this));
}
}
}
The latter part takes care of DependencyProperties
themselves. We are not writing any CLR-Properties here. Why? Simply because we already covered them in the first part: Either, it's a standard property which is backed by a private
member anyway, or it is an auto-property which will generate its own backing automatically, also appearing in the list of properties. The latter limits flexibility, since going from an auto property to a non-auto property will make them incompatible. But that is a larger issue, really.
Now, we're not interested in any read-only DPs, nor any properties that are declared at the level of (the non-serializable) DependencyObject
and above. Moreover, we have to check for the NotSerialized
Attribute
, which can only be found using the FieldInfo
:
private bool IsSerializableDependencyProperty(PropertyDescriptor _descriptor)
{
if (_descriptor.IsReadOnly)
return false;
DependencyProperty dp =
DependencyPropertyHelper.FindSerializableDependencyProperty
(this, _descriptor.Name);
return (dp != null);
}
private static object FindDependencyPropertyInternal(object _object,
string _propertyName,
bool _acceptNotSerialized)
{
if (null != _object && !String.IsNullOrEmpty(_propertyName))
{
Type typeAttachedTo = _object.GetType();
string propertyPropertyName = _propertyName + "Property";
FieldInfo fi = null;
while (null == fi && typeAttachedTo != typeof(DependencyObject))
{
fi = typeAttachedTo.GetField(propertyPropertyName);
if (null != fi && (fi.Attributes & FieldAttributes.NotSerialized) ==
FieldAttributes.NotSerialized)
{
return null;
}
typeAttachedTo = typeAttachedTo.BaseType;
}
if (null != fi)
{
return fi.GetValue(null);
}
}
return null;
}
For deserialization purposes, we can implement very similar code for the deserialization constructor:
[Serializable]
public class SerializableDependencyObject : DependencyObject, ISerializable
{
public SerializableDependencyObject
(SerializationInfo info, StreamingContext context)
{
Type thisType = GetType();
List<FieldInfo> fieldInformation =
ReflectionHelper.GetSerializableFieldInformation(thisType);
foreach (FieldInfo fieldInformationRover in fieldInformation)
{
fieldInformationRover.SetValue(this,
info.GetValue(fieldInformationRover.Name,
fieldInformationRover.FieldType));
}
PropertyDescriptorCollection descriptors = TypeDescriptor.GetProperties(
thisType,
new Attribute[]
{
new PropertyFilterAttribute(PropertyFilterOptions.SetValues |
PropertyFilterOptions.UnsetValues |
PropertyFilterOptions.Valid)
});
foreach (PropertyDescriptor propertyDescriptor in descriptors)
{
if (!IsSerializableDependencyProperty(propertyDescriptor))
continue;
DependencyProperty dp =
DependencyPropertyHelper.FindDependencyProperty(this, propertyDescriptor.Name);
if (null != dp)
{
SetValue(dp, info.GetValue(propertyDescriptor.Name,
propertyDescriptor.PropertyType));
}
else
{
throw new SerializationException(String.Format
("Failed to deserialize property '{0}' on object of type '{1}'.
Property could not be found. Version Conflict?",
propertyDescriptor.Name, thisType));
}
}
}
}
So now, we eliminated the need to specify correct implementations of GetObjectData()
for each and every class, but we still need to provide a serialization constructor - a simple empty one, but still a potential source of run-time errors. What we need to provide for every derived class is this:
[Serializable]
public class GraphNode : GraphNodeBase
{
public GraphNode(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
Make sure not to forget this one! For debugging purposes, you may want to activate breaks at first-chance exceptions, otherwise a missing serialization constructor -or much worse a misbehaving one- can be a real pain...
The attached sample application contains two executable projects. The first, "WPFDependencyPropertySerialization
" will create a tree of GraphNodes
, serialize it, and de-serialize it again. The trees are displayed in a simple console output both times, so you can easily compare them. Make sure not to increase the size dramatically, because the console output will take a long time...
The second, "WPFDependencyPropertySerializationError
" shows a problem which I am going to explain further at the bottom of this article. Both projects share a very simple 'profiler' and a small 'shared' library that contains the DependencyPropertyHelper
and the ReflectionHelper
.
Already? Yes, almost. With the code supplied so far, we can provide a base class that takes care of serialization and de-serialization pretty well. What remains is the necessity to provide a serialization constructor in every derived class. Also, each derived class must be marked [Serializable]
.
While all this is quite convenient, some limitations may apply: Since we use DependencyObject's
SetValue()
-method, we might trigger validation callbacks during de-serialization. This might wreak havoc, so be careful which features you use. The code for this can be downloaded at the top of the article. The Project "WPFDependencyPropertySerialization
" contains everything discussed so far.
Note that there is some profiling and some debugging code in the project. These two obviously don't go too well together, but I figured debugging code 'disappears' in release builds anyway so you can perform some speed-measurement with your own classes. On the other hand, while profiling in debug is pointless, it does not really present a limitation, either.
Unfortunately, this is as far as I got. Of course, the code is simplistic and contains hacks here and there so there we have space for improvement. But when it comes to getting rid of the de-serialization constructor, there appears to be no working solution. I'll show the remaining problems in the rest of this article.
There is one more concept in .NET serialization that is very interesting:
Instead of making an object itself serializable, we can provide a delegate for serialization purposes that implements ISerializationSurrogate
- an interface very similar to ISerializable
:
GetObjectData()
- Populates the provided SerializationInfo
with the data needed to serialize the object. SetObjectData()
- Populates the object using the information in the SerializationInfo
.
On serialization and de-serialization, the formatter searches for a surrogate for the given type and asks the surrogate to handle serialization. Since the surrogate also has a SetObjectData()
method, we don't need to provide a serialization constructor in our classes anymore, thus making our code a lot simpler. Again, we can immediately apply the reflection-driven version and create our ReflectionSerializationSurrogate
:
public sealed class ReflectionSerializationSurrogate : ISerializationSurrogate
{
#region ISerializationSurrogate Members
public void GetObjectData
(object obj, SerializationInfo info, StreamingContext context)
{
PropertyDescriptorCollection descriptors =
TypeDescriptor.GetProperties(obj.GetType(),
new Attribute[] { new PropertyFilterAttribute
(PropertyFilterOptions.SetValues |
PropertyFilterOptions.UnsetValues |
PropertyFilterOptions.Valid ) });
List<FieldInfo> fieldInformation =
ReflectionHelper.GetSerializableFieldInformation(obj.GetType());
Debug.Print("SerializableDependencyObject.GetObjectData invoked.");
Debug.Print("Serializing object of type: {0}", obj.GetType().Name);
foreach (FieldInfo fiRover in fieldInformation)
{
Debug.Print("\tSerializing member '{0}'", fiRover.Name);
info.AddValue(fiRover.Name, fiRover.GetValue(obj));
}
DependencyObject dependencyObject = obj as DependencyObject;
if (null != dependencyObject)
{
foreach (PropertyDescriptor propertyDescriptor in descriptors)
{
if (!DependencyPropertyHelper.IsSerializableDependencyProperty
(dependencyObject, propertyDescriptor))
continue;
Debug.Print("\tSerializing property '{0}'", propertyDescriptor.Name);
info.AddValue(propertyDescriptor.Name, propertyDescriptor.GetValue(obj));
}
}
}
public object SetObjectData(object obj, SerializationInfo info,
StreamingContext context, ISurrogateSelector selector)
{
Type thisType = obj.GetType();
DependencyObject dependencyObject = obj as DependencyObject;
DependencyObject doTest2 = (DependencyObject)Activator.CreateInstance(thisType);
List<FieldInfo> fieldInformation =
ReflectionHelper.GetSerializableFieldInformation(thisType);
foreach (FieldInfo fiRover in fieldInformation)
{
fiRover.SetValue(obj, info.GetValue(fiRover.Name, fiRover.FieldType));
}
if (null == dependencyObject)
return obj;
PropertyDescriptorCollection descriptors = TypeDescriptor.GetProperties(thisType,
new Attribute[] { new PropertyFilterAttribute
(PropertyFilterOptions.SetValues |
PropertyFilterOptions.UnsetValues |
PropertyFilterOptions.Valid ) });
List<DependencyValueMap> mappingList = new List<DependencyValueMap>();
foreach (PropertyDescriptor propertyDescriptor in descriptors)
{
if (!DependencyPropertyHelper.IsSerializableDependencyProperty
(dependencyObject, propertyDescriptor))
continue;
if (propertyDescriptor.Attributes[typeof(NonSerializedAttribute)] == null)
{
DependencyProperty dp = DependencyPropertyHelper.FindDependencyProperty
(obj, propertyDescriptor.Name);
if (null != dp)
{
mappingList.Add(new DependencyValueMap(dp,
obj as DependencyObject,
info.GetValue
(propertyDescriptor.Name,
propertyDescriptor.
PropertyType)));
}
else
{
throw new SerializationException(String.Format
(@"Failed to deserialize property '{0}' on object of type '{1}'.
Property could not be found. Version Conflict?",
propertyDescriptor.Name, thisType));
}
}
}
foreach (DependencyValueMap dvRover in mappingList)
{
dependencyObject.SetValue(dvRover.dp, dvRover.value);
}
return dependencyObject;
}
}
All we need to do now is register the surrogate with the surrogate selector:
ISurrogateSelector selector = new SurrogateSelector();
StreamingContext sc = new StreamingContext(StreamingContextStates.All);
selector.AddSurrogate(typeof(SerializableDependencyObject),
sc, new GraphNodeBaseSerializationSurrogate());
BinaryFormatter bf = new BinaryFormatter();
bf.SurrogateSelector = selector;
We're almost done, but trying to serialize the example object fails - why? Well the SurrogateSelector
wants exact matches on the type supplied - which we can't guarantee. Fortunately, we can implement our own ISurrogateSelector
that applies our ReflectionSerializationSurrogate
to all derived types:
class MySurrogateSelector : ISurrogateSelector
{
ISurrogateSelector mNextSelector;
ReflectionSerializationSurrogate mSurrogate =
new ReflectionSerializationSurrogate();
#region ISurrogateSelector Members
public void ChainSelector(ISurrogateSelector selector)
{
mNextSelector = selector;
}
public ISurrogateSelector GetNextSelector()
{
return mNextSelector;
}
public ISerializationSurrogate GetSurrogate(Type type,
StreamingContext context,
out ISurrogateSelector selector)
{
if (type.IsSubclassOf(typeof(SerializableDependencyObject)))
{
selector = this;
return mSurrogate;
}
selector = null;
return null;
}
#endregion
}
Now, all seems to be set up, so we just hit "F5" in the attached source's "WPFDependencyPropertySerializationError
" project and find -it crashes- with an InvalidOperationException
"Current local value enumeration is outdated because one or more local values have been set since its creation.".
This code is still heavily simplified, of course. 'Real' serialization has to deal with a lot of important issues related to the business data itself, such as conflicting versions. Also, note that DependencyProperties
with complex validation or side-effects on change may cause trouble since you can't control the point in time they are set, exactly.
For most of these, simple solutions can be found. I guess one can employ some fancy tricks such as fiddling around with Reflection.Emit
to create serialization constructors on-the-fly, or at least verifying all classes derived from SerializableDependencyObject
implement one.
- 2009-03-05 v. 1.2 More bugfixes, cleaned up the code a little
- 2009-03-05 v. 1.1 Important updates, clarified a few things, updated code
- 2009-03-05 v. 1.0 Initial release