Click here to Skip to main content
Click here to Skip to main content

DependencyProperty Serialization for Business Objects

, 5 Mar 2009 CPOL
Rate this:
Please Sign up or sign in to vote.
Presents a simple way to serialize business objects that are derived from WPF's DependencyObject and use DependencyProperty with the help of some reflection

Contents

Introduction

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.

Background

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.

Original Problem

.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<graphnodebase /> 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.

The Aim

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) 
{
    // Delegate all the hard tasks to a BinaryFormatter
    BinaryFormatter formatter = new BinaryFormatter();

    // Serialize the object graph
   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.

Reflection, Help!

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))
      {
        // Retrieve all instance fields. 
        // This will present us with quite a lot of stuff, such as backings for 
        // auto-properties, events, etc.
        FieldInfo[] fields = typeRover.GetFields
			(BindingFlags.Instance | BindingFlags.Public | 
                            BindingFlags.NonPublic | BindingFlags.DeclaredOnly);

        foreach (FieldInfo fiRover in fields)
        {
          if (fiRover.IsNotSerialized)
              continue;

          // Make sure we don't serialize events here, 
	 // because that will drag along half of 
          // your forms, views, presenters or whatever is used for user interaction....
          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)
{
    // Quick exit if possible
    if (_descriptor.IsReadOnly)
        return false;

    DependencyProperty dp = 
      DependencyPropertyHelper.FindSerializableDependencyProperty
					(this, _descriptor.Name);

    return (dp != null);
}

// And in the DependencyPropertyHelper:

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)
      {
        // bail out: We found the property, but it's marked as NonSerialized, 
        // so we're done.
        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();

    // De-Serialize Fields
    List<FieldInfo> fieldInformation = 
	ReflectionHelper.GetSerializableFieldInformation(thisType);

    foreach (FieldInfo fieldInformationRover in fieldInformation)
    {
        fieldInformationRover.SetValue(this, 
                                       info.GetValue(fieldInformationRover.Name, 
                                       fieldInformationRover.FieldType));
    }

    // De-Serialize DependencyProperties
    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 Sample Application

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.

The End

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.

An Attempt

There is one more concept in .NET serialization that is very interesting:

SerializationSurrogates

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;
    
    // Setting the data works with this one... But the formatter will not re-stitch the 
    // object graph..
    DependencyObject doTest2 = (DependencyObject)Activator.CreateInstance(thisType);

    // De-Serialize Fields
    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;

    //
    // De-Serialize DependencyProperties
    //
    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)
    {
      // That line will crash:
      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
}

Serious Trouble

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.".

Outlook

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.

Thank you for reading!

History

  • 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Christoph Menge
Software Developer emphess
Germany Germany
Chris lives in Munich, Germany and tries to study Physics.
 
Alongside, he works as a self-employed software consultant on projects from various fields.
 
Unfortunately, he loved software ever since he started with C++, and his addiction has become a lot worse since the introduction of .NET 2.0.
 
In his spare time, he tries to go sailing, drink some nice single malt whisky and, in very rare cases, he also sleeps.
 
You can reach him at ChrisNOSPAM at emphess.net or via his blog emphess.net.

Comments and Discussions

 
SuggestionSolution for the surrogate problem... PinmemberH311F14m313-Oct-11 17:17 
GeneralSuperb help... PinmemberprabodhMInz28-May-09 7:46 
GeneralSerious Trouble Pinmembertimtos227-May-09 8:44 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.150327.1 | Last Updated 5 Mar 2009
Article Copyright 2009 by Christoph Menge
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid