65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (6投票s)

2009 年 3 月 5 日

CPOL

8分钟阅读

viewsIcon

27403

downloadIcon

239

借助反射, 提供了一种对派生自 WPF 的 DependencyObject 并使用 DependencyProperty 的业务对象进行序列化的简单方法

目录

引言

在本文中,我将介绍一种简单的方法,借助一些反射来序列化派生自 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,并将其插入 DependencyObjectGraphNodeBase 之间。

[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”和一个包含 DependencyPropertyHelperReflectionHelper 的小型“shared”库。

结束

已经完成了?是的,几乎完成了。有了迄今为止提供的代码,我们可以提供一个很好地处理序列化和反序列化的基类。剩下的就是每个派生类都必须提供一个序列化构造函数。此外,每个派生类都必须标记为 [Serializable]

虽然这一切都相当方便,但可能会有一些限制:由于我们使用 DependencyObjectSetValue() 方法,我们在反序列化期间可能会触发验证回调。这可能会造成混乱,因此请小心您使用的功能。代码可在文章顶部下载。项目“WPFDependencyPropertySerialization”包含到目前为止讨论的所有内容。

请注意,项目中包含了一些性能分析调试代码。这两者显然不太兼容,但我认为调试代码在发布版本中会“消失”,因此您可以对自己的类进行一些速度测量。另一方面,虽然在调试模式下进行性能分析毫无意义,但它并没有真正构成限制。

不幸的是,我就是这样。当然,代码是简化的,并且在某些地方包含了一些技巧,因此有改进的空间。但是,当谈到摆脱反序列化构造函数时,似乎没有可行的解决方案。我将在本文的其余部分展示剩余的问题。

一次尝试

在 .NET 序列化中还有一个非常有趣的方面:

SerializationSurrogates

我们不是让对象本身可序列化,而是为序列化目的提供一个实现 ISerializationSurrogate 的委托——一个与 ISerializable 非常相似的接口。

GetObjectData() - 使用序列化对象所需的数据填充提供的 SerializationInfoSetObjectData() - 使用 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 初始发布
© . All rights reserved.