无法序列化控件上的属性
背景
我正在尝试创建我在VB.NET中创建的业务对象的副本。我已经实现了ICloneable接口,并在Clone函数中创建了对象的副本,方法是使用BinaryFormatter对其进行序列化,然后直接将其反序列化为另一个从该函数返回的对象。
我要序列化的类以及包含在该类中的子对象被标记为"可序列化"。
我已经通过编写类似于以下内容的代码测试了clone方法的工作原理:
Dim obj as New Sheep() Dim dolly as Sheep = obj.Clone()
此时一切正常。
问题
我有一个自定义的Windows窗体控件,该控件继承自第3方控件。这个自定义控件基本上包含我要克隆的对象(因为该对象最终将馈给第三方控件)。
我想在Windows窗体控件中创建对象的克隆,以便允许用户操纵属性,同时可以选择取消更改并将对象恢复为进行更改之前的状态。我想在用户开始进行更改之前先保留对象的副本,然后保留该对象,这样如果他们按取消,就可以准备好它。
我的想法是按照以下方式编写代码:
Dim copy as Sheep = MyControl.Sheep.Clone()
然后允许用户操纵" MyControl.Sheep"上的属性。但是,当我尝试执行此操作时,clone方法将引发异常,指出:
程序集" My_Assembly_Info_Here"中的类型" MyControl"未标记为可序列化
我调用BinaryFormatter.Serialize(stream,Me)时抛出该错误。
我试过在MyControl上创建一个方法,该方法返回对象的副本,并且还首先将MyControl.Sheep分配给另一个变量,然后克隆该变量,但似乎无济于事。但是,直接创建对象的新实例并进行克隆可以正常工作!
知道我哪里出问题了吗?
解决方案
马克的回答帮助我在这个问题上指明了正确的方向。洛基·洛特卡(Rocky Lhotka)的这篇博客文章解释了该问题以及解决方法。
解决方案
一个明显的问题,但是我们确定Sheep对象中没有MyControl的引用。是对象还是列表,还是其他任何东西?在这种情况下,这就是阻止我们克隆业务对象的原因。
.Parent或者.Tag属性是最有可能的候选者。
我们是否有UI订阅的事件?一个{Foo} Changed事件,如果有数据绑定,或者可能是INotifyPropertyChanged?
我们可能必须将事件支持字段标记为[NonSerialized](或者属性在VB中看起来像是Cperson ...)。如果我们使用的是类似字段的事件(即不添加/删除的缩写语法),请使用[field:NonSerialized]标记整个事件(再次,转换为VB)。
马克
在第三方库中,如果某些东西未标记为可序列化,则不应出于充分的原因将其序列化,但由于开发人员只是根本不将其包括进来,因此通常无法序列化。我们可以使用反射来复制控件的公共属性,并在取消时将其状态返回到反射版本。这种方法会对性能产生影响,但是由于我们正在UI层工作,所以我认为它不会带来太大的麻烦。这种方法不能保证没有错误。公共属性不一定代表类的整个状态,而设置某些属性可能会有副作用(它们不应该,但是我们没有编写代码,因此请ILDAsm查看并希望取得最好的效果)。
另外,并非所有类型的属性都可以序列化,在这种情况下,我们需要进一步手动编写针对这些类型的序列化例程(可能还有那些类型的属性)。
using System;
using System.Collections.Generic;
using System.Linq;
namespace ConsoleApplication1
{
public class NonSerializableSheep
{
public NonSerializableSheep() { }
public string Name { get; set; }
public int Id { get; set; }
// public read only properties can create a problem
// with this approach if another property or (worse)
// a group of properties sets it
public int Legs { get; private set; }
public override string ToString()
{
return String.Format("{0} ({1})", Name, Id);
}
}
public static class GhettoSerializer
{
// you could make this a factory method if your type
// has a constructor that appeals to you (i.e. default
// parameterless constructor)
public static void Initialize<T>(T instance, IDictionary<string, object> values)
{
var props = typeof(T).GetProperties();
// my approach does nothing to handle rare properties with array indexers
var matches = props.Join(
values,
pi => pi.Name,
kvp => kvp.Key,
(property, kvp) =>
new {
Set = new Action<object,object,object[]>(property.SetValue),
kvp.Value
}
);
foreach (var match in matches)
match.Set(instance, match.Value, null);
}
public static IDictionary<string, object> Serialize<T>(T instance)
{
var props = typeof(T).GetProperties();
var ret = new Dictionary<string, object>();
foreach (var property in props)
{
if (!property.CanWrite || !property.CanRead)
continue;
ret.Add(property.Name, property.GetValue(instance, null));
}
return ret;
}
}
public class Program
{
public static void Main()
{
var nss = new NonSerializableSheep
{
Name = "Dolly",
Id = 12
};
Console.WriteLine(nss);
var bag = GhettoSerializer.Serialize(nss);
// a factory deserializer eliminates the additional
// declarative step
var nssCopy = new NonSerializableSheep();
GhettoSerializer.Initialize(nssCopy, bag);
Console.WriteLine(nssCopy);
Console.ReadLine();
}
}
}

