业务对象的依赖项属性序列化






4.50/5 (6投票s)
借助反射,
目录
引言
在本文中,我将介绍一种简单的方法,借助一些反射来序列化派生自 System.Windows.DependencyObject
并使用 System.Windows.DependencyProperty
的业务对象。
背景
我正在进行的一个项目依赖于 DependencyProperties
来表示数据。这是一个有争议的设计决定,因为它使数据依赖于特定的表示层。另一方面,它可以防止您重新发明轮子。不过,这个讨论会延伸得太远,超出了本文的范围。
原始问题
.NET 序列化本身就非常强大。为什么?仅仅因为 BinaryFormatter
能够自动序列化和反序列化对象图,从而节省您创建对象唯一 ID、查找循环链接、处理泛型列表、按正确顺序反序列化等工作。这些繁琐的问题已经为我们解决了,所以如果我们能利用这些功能,再次节省我们的时间,那就太好了。
但是,请注意,并非所有格式化程序都能做到这一点。例如,SoapFormatter
无法处理泛型列表。就目前而言,BinaryFormatter
绝对是开箱即用的功能最全的格式化程序,所以我们将在其中使用它。
在此示例中,我们将处理一个树形结构,因为它完美地展示了 BinaryFormatter
的强大功能。请注意,节点具有循环链接,因为子节点知道其父节点,反之亦然。还请注意泛型列表的使用。
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; }
}
// ...
}
代码分为两个类,以使问题更现实,因为我们需要使用反射遍历继承树。稍后会详细介绍。注意只读 CLR 属性和自动属性。完整的源代码包含一个带有更多成员的 GraphNode
,以便提供更好的概览。
目标
现在,我基本上想看到 MSDN 最简单示例中提供的用于序列化和反序列化的代码,类似这样:
void SaveGraph(Stream stream)
{
// Delegate all the hard tasks to a BinaryFormatter
BinaryFormatter formatter = new BinaryFormatter();
// Serialize the object graph
formatter.Serialize(stream, MyGraphRootNode);
}
如果 GraphNode
的名称不是 DependencyProperty
,则上面的代码将(几乎)正常工作。但是,由于 GraphNodeBase
派生自 DependencyObject
,因此序列化将在运行时失败,并抱怨类 DependencyObject
未标记为可序列化。现在,我们卡住了,因为如果我们不从 DependencyObject
派生,我们就无法使用 DependencyProperty
。现在怎么办?
现在,不幸的是,虽然您可以通过 [NonSerialized]
属性简单地阻止聚合对象在序列化过程中被考虑在内,但除非您通过派生类中的 ISerializable
接口手动实现某些序列化步骤,否则无法简单地阻止基类被序列化。所以让我们这样做,最终得到类似这样的结果:
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);
}
}
乍一看,这是一个可接受的解决方案,但它迫使我们编写大量愚蠢且因此易出错的代码:在每个类中,我们需要重写 GetObjectData()
并添加相应的信息,不要忘记调用基类(这会导致混乱)!哦,我们刚刚杀死了著名的 [NonSerialized]
属性。此外,我们需要提供一个反序列化构造函数。缺少后者会导致反序列化时出现运行时错误,这使得问题更糟。
反射,救命!
这个简单的例子可能不太令人信服,但对于具有大量成员的复杂对象来说,这非常令人不安。更不用说修改类时的额外开销了!反射可以帮助我们!使用反射,我们可以为每个类型组合要序列化的成员列表。
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;
}
}
我们告诉 GetFields()
方法为我们提供所有实例成员,无论它们是否为 public
(与仅处理 public
成员的 XamlWriter
不同),但只包括在相应类中声明的项目(使用 DeclaredOnly
标志)。为了性能起见,我们将结果缓存到 Hashtable
中。上面的代码将在达到 SerializableDependencyObject
级别时停止,因此我们不会深入到 Object
类。还请注意,我们必须在此处和那里进行一些调整。例如,序列化事件是一个非常糟糕的主意,因此我们必须考虑到这一点。
有了这些代码,我们就可以通过在基类中实现 GetObjectData()
并序列化对象中所有未标记 NonSerializedAttribute [NonSerialized]
的属性来模拟默认序列化的行为。我们称此类为 SerializableDependencyObject
,并将其插入 DependencyObject
和 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));
}
}
}
后一部分负责处理 DependencyProperties
本身。我们在这里没有编写任何 CLR 属性。为什么?仅仅因为我们已经在第一部分中涵盖了它们:要么,它是一个标准的属性,它由一个 private
成员支持,要么它是一个自动属性,它会自动生成自己的支持,也会出现在属性列表中。后者限制了灵活性,因为从自动属性转为非自动属性会使它们不兼容。但这确实是一个更大的问题。
现在,我们不关心任何只读 DP,也不关心在(不可序列化的)DependencyObject
及其以上的级别声明的任何属性。此外,我们必须检查 NotSerialized
Attribute
,它只能使用 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;
}
为了反序列化,我们可以为反序列化构造函数实现非常相似的代码:
[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));
}
}
}
}
现在,我们消除了为每个类指定正确实现 GetObjectData()
的需要,但仍然需要提供一个序列化构造函数 - 一个简单的空构造函数,但仍可能导致运行时错误。我们为每个派生类提供的就是这个:
[Serializable]
public class GraphNode : GraphNodeBase
{
public GraphNode(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
// ...
}
请务必不要忘记这个!为了调试目的,您可能希望在首次捕获异常时激活断点,否则缺少序列化构造函数——或者更糟的是一个行为不正常的构造函数——可能会非常令人头疼。
示例应用程序
附加的示例应用程序包含两个可执行项目。第一个“WPFDependencyPropertySerialization
”将创建一个 GraphNodes
树,将其序列化,然后再次反序列化。树在两次运行时都显示在简单的控制台输出中,因此您可以轻松地进行比较。请勿大幅增加大小,因为控制台输出将花费很长时间。
第二个“WPFDependencyPropertySerializationError
”显示了我将在本文底部进一步解释的一个问题。两个项目共享一个非常简单的“profiler”和一个包含 DependencyPropertyHelper
和 ReflectionHelper
的小型“shared”库。
结束
已经完成了?是的,几乎完成了。有了迄今为止提供的代码,我们可以提供一个很好地处理序列化和反序列化的基类。剩下的就是每个派生类都必须提供一个序列化构造函数。此外,每个派生类都必须标记为 [Serializable]
。
虽然这一切都相当方便,但可能会有一些限制:由于我们使用 DependencyObject
的 SetValue()
方法,我们在反序列化期间可能会触发验证回调。这可能会造成混乱,因此请小心您使用的功能。代码可在文章顶部下载。项目“WPFDependencyPropertySerialization
”包含到目前为止讨论的所有内容。
请注意,项目中包含了一些性能分析和调试代码。这两者显然不太兼容,但我认为调试代码在发布版本中会“消失”,因此您可以对自己的类进行一些速度测量。另一方面,虽然在调试模式下进行性能分析毫无意义,但它并没有真正构成限制。
不幸的是,我就是这样。当然,代码是简化的,并且在某些地方包含了一些技巧,因此有改进的空间。但是,当谈到摆脱反序列化构造函数时,似乎没有可行的解决方案。我将在本文的其余部分展示剩余的问题。
一次尝试
在 .NET 序列化中还有一个非常有趣的方面:
SerializationSurrogates
我们不是让对象本身可序列化,而是为序列化目的提供一个实现 ISerializationSurrogate
的委托——一个与 ISerializable
非常相似的接口。
GetObjectData()
- 使用序列化对象所需的数据填充提供的 SerializationInfo
。SetObjectData()
- 使用 SerializationInfo
中的信息填充对象。
在序列化和反序列化时,格式化程序会查找给定类型的代理,并要求代理处理序列化。由于代理还有一个 SetObjectData()
方法,我们不再需要在类中提供序列化构造函数,从而使我们的代码更加简洁。同样,我们可以立即应用基于反射的版本并创建我们的 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;
}
}
现在,我们只需要将代理注册到代理选择器:
ISurrogateSelector selector = new SurrogateSelector();
StreamingContext sc = new StreamingContext(StreamingContextStates.All);
selector.AddSurrogate(typeof(SerializableDependencyObject),
sc, new GraphNodeBaseSerializationSurrogate());
BinaryFormatter bf = new BinaryFormatter();
bf.SurrogateSelector = selector;
// ...
我们快完成了,但尝试序列化示例对象会失败——为什么?嗯,SurrogateSelector
要求与提供的类型完全匹配——我们无法保证这一点。幸运的是,我们可以实现自己的 ISurrogateSelector
,它将我们的 ReflectionSerializationSurrogate
应用于所有派生类型:
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
}
严重麻烦
现在,一切似乎都已就绪,所以我们只需按附加源代码中的“WPFDependencyPropertySerializationError
”项目的“F5”键,然后发现——它崩溃了——并出现 InvalidOperationException
“当前的本地值枚举已过时,因为自其创建以来已设置了一个或多个本地值。”。

Outlook(展望)
当然,这段代码仍然被大大简化了。“真正的”序列化必须处理许多与业务数据本身相关的重要问题,例如版本冲突。另外,请注意,具有复杂验证或更改时产生副作用的 DependencyProperties
可能会导致问题,因为您无法精确控制设置它们的时间点。
对于这些大多数问题,都可以找到简单的解决方案。我猜我们可以采用一些巧妙的技巧,例如通过 Reflection.Emit
来动态创建序列化构造函数,或者至少验证所有派生自 SerializableDependencyObject
的类都实现了一个。
感谢阅读!
历史
- 2009-03-05 v. 1.2 更多 bug 修复,清理了代码
- 2009-03-05 v. 1.1 重要更新,澄清了一些问题,更新了代码
- 2009-03-05 v. 1.0 初始发布