自定义 .NET XML 序列化库






4.43/5 (4投票s)
2006 年 1 月 25 日
11分钟阅读

43330

465
介绍了一个自定义 XML 序列化库,该库具有比较差异和合并差异的功能
引言
当我最初着手编写这段代码时,我想要编写一个客户端/服务器应用程序。我考虑使用 XML 序列化将服务器状态发送到客户端。 .NET 中的 XML 序列化非常灵活,能够序列化和反序列化几乎任何 .NET 类型,并且在 XML 格式化方式上提供了很多控制。然而,将整个服务器状态通过网络发送到客户端效率不高;我想要一种方法来仅发送自上次更新以来与之前状态的差异。
我需要以下功能
- XML 输出必须符合固定的格式,具有特定的命名和排序约定,这样任何两个 XML 源都可以快速有效地进行差异比较。
- 在 XML 差异文档中,XML 必须存储关于差异状态(添加、删除、更新以及“无更改”占位符)的信息,以及被删除的先前值。
- 能够递归遍历现有的 XML 文档或数据结构,比较或合并任何差异。
因此,我编写了一个简单的 XML 序列化库,名为“Wml
”,以支持这些功能。它基于 .NET 现有的 XML 和序列化类构建,并尽可能遵循它们的约定。在编写过程中,我发现能够递归遍历 Wml
结构,并能够计算和合并差异还有其他一些用途。
在本文中,我将介绍 Wml
的一些基础知识,如何使 .NET 类型能够被序列化为 Wml
,以及 Wml
序列化库的一些用途。
Wml
Wml
库序列化到的 XML 的受限形式称为“Wml
”。
示例
<Wml>
<DirectReports I="2" V="Wxv.WmlDemo.JobPosition">
<FirstName V="Daniel" />
<Id V="2" />
<LastName V="Taylor" />
</DirectReports>
<FirstName V="Christopher" />
<Id V="0" />
<LastName V="Wilson" />
<zz. I="1" V="Wxv.WmlDemo.JobPosition">
<FirstName V="Isabella" />
<Id V="1" />
<LastName V="Jones" />
</zz.>
</Wml>
包含差异状态的示例
<Wml>
<!-- updating the name from chairperson to director -->
<JobTitle V="Director" D="Chairperson" S="U" />
<!-- addition of a new employee -->
<zz. I="1" V="Wxv.WmlDemo.JobPosition" S="A">
<FirstName V="Isabella" S="A" />
<LastName V="Jones" S="A" />
</zz.>
</Wml>
其基本结构是
- 根元素名称始终为“
Wml
”。 - 元素名称对应于字段或属性名称,或者集合项的“
zz
.”。 - 两个主要属性是
- “Identity” (I) - 一个表示唯一对象标识的整数。默认值为
-1
。 - “Value” (V) - 要么是值的
string
表示形式,要么是 .NET 类型的完整类型名称。默认值为null
。
- “Identity” (I) - 一个表示唯一对象标识的整数。默认值为
- 子节点必须按名称和 ID 排序并唯一。
- 差异文档使用另外两个属性
- “State” (S) - “Added” (A)(添加)、“Deleted” (D)(删除)、“Updated” (U)(更新),或“No Change” (N)(无更改)。默认值为“No Change”。
- “DeletedValue” (D) - 成员的先前值,用于回滚任何差异。默认值为
null
。
Wml
具有这种固定的结构和约束,以便任何两个 Wml
源都能有效地进行差异比较。
类似于 XML
- 可以使用
WmlDocument
类将其保存在 DOM 中,该类包含WmlDomNode
s。 - 可以使用
WmlTextReader
和WmlTextWriter
将其读写到文本源或目标。 Wml
DOM 本身可以使用WmlDomNodeReader
或WmlDomNodeWriter
进行读写。- 节点实现了一个
IWmlNode
接口,其概念类似于IXPathNavigable
,这允许WmlNodeReader
或WmlNodeWriter
(WmlDomNodeReader
或WmlDomNodeWriter
从中继承)访问节点,而无需了解Wml
节点信息所保存的基础结构或类型。
序列化类型
要使一个类型(class
或 struct
)可序列化,它必须实现 IWmlSerializable
接口。
public interface IWmlSerializable
{
int GetHashCode();
}
类型中只需要实现一个方法,即 IWmlSerializable.GetHashCode()
,它强制任何实现者覆盖 object.GetHashCode()
。此函数应返回一个整数,该整数对应于实例相对于任何其他实例或 null
值(其被视为具有默认哈希码或标识值 -1
)的唯一 *identity*。 .NET 中的 XML 序列化不需要此概念,因为它总是从头开始反序列化新的数据结构,但它在 Wml
中将差异合并到现有结构时很重要。哈希码结果在实例生命周期内不应改变,并且其值基于一个也序列化的成员值,因此可以在反序列化后可靠地重新计算。
获得对象唯一标识的最简单方法是在实例化时为“ID
”字段或属性分配一个自动递增的整数值。它也可以从它保存的唯一且恒定的数据值计算得出,或者基于它所在的集合或数组的索引(只要它是该索引处的唯一引用,包括 null
)。
与 XML 序列化一样,任何 IWmlSerializable
类型还必须定义一个无参数构造函数,以便在反序列化期间自动实例化类型。
默认情况下,Wml
序列化会序列化 IWmlSerializable
类型上定义的任何非静态的 public
成员(字段或属性),只要其值可以被读取和写入。这包括 IWmlSerializable
引用,以及任何其他 .NET 类 TypeConverter
可以将其值转换为字符串并从字符串转换回的“基本”类型。
您不希望被序列化的成员(例如,它们包含临时或派生信息)可以用 WmlIgnore
属性标记。
例如
public class JobPosition : IWmlSerializable
{
private static int MaxId = 0;
private int id = -1;
public int Id
{
get { if (id == -1) id = MaxId++; return id; }
set { id = value; }
}
public override int GetHashCode()
{
return Id;
}
public string FirstName;
public string LastName;
public string JobTitle;
public DateTime DateStarted;
public enum GenderEnum { Male = 0, Female = 1 }
public GenderEnum Gender;
public JobPosition DirectReports;
[WmlIgnore()]
public int Tag;
}
如果集合类型需要序列化它包含的任何子数据对象,它可以实现 IWmlSerializableCollection
接口。
public interface IWmlSerializableCollection : IWmlSerializable,
IEnumerable
{
IWmlSerializable Get (int id);
void Remove (int id);
void Add (IWmlSerializable item);
}
id 参数对应于 IWmlSerializable
实例返回的 GetHashCode()
值。IWmlSerializableCollection
实例不得包含 null
项,并且其枚举器必须按“id
”顺序返回 IWmlSerialiable
集合项。
例如
public class JobPosition : IWmlSerializable, IWmlSerializableCollection {
/* other code here */
private SortedList manages = new SortedList();
public IWmlSerializable Get(int id)
{
return Get (id);
}
public void Remove (int id)
{
manages.Remove (id);
}
public void Add (IWmlSerializable item)
{
Add ((JobPosition) item);
}
public IEnumerator GetEnumerator()
{
return manages.Values.GetEnumerator();
}
}
(尽管通常您会提供这些方法的类型安全版本并隐藏 IWmlSerializableCollection
实现。)
当一个类型实现 IWmlSerializable
并可选地实现 IWmlSerializableCollection
时,它允许 Wml
代码在该类型上构建一个名为 WmlSerializableNode
的视图。与 Wml DOM 节点一样,此类实现了 IWmlNode
,这允许它被 WmlNodeReader
和 WmlNodeWriter
以与 Wml DOM 相同的方式处理。IWmlSerializable
实例的重写的 WmlNodeReader
和 WmlNodeWriter
类是 WmlSerializableNodeReader
和 WmlSerializableNodeWriter
。
IWmlSerializable
中的每个实例都应该只被一个序列化成员或集合引用一次,例如,不允许有指向同一实例的重复引用或循环引用。这由 WmlSerializableNodeReader
进行验证,如果您尝试序列化无效数据结构,它会引发异常。
WmlSerializer
这是一个 abstract
类,包含执行各种 Wml
相关实用函数的 static
方法,主要利用 Wml
读取器和写入器。有八种方法(已重载以支持 WmlDocument
或 IWmlSerializable
实例,或 Wml
读取器和写入器)。在此列表中,“Wml Structure”指的是 WmlDocument
和 IWmlSerializable
实例。
Equals
- 比较两个Wml
结构是否相等Compare
- 比较两个Wml
结构并生成Wml
差异文档Combine
- 将Wml
差异合并到现有的Wml
结构中Copy
- 将输入从WmlReader
源复制到目标WmlWriter
Clone
- 创建Wml
结构的深层副本Serialize
- 将Wml
结构保存到写入器或文档Deserialize
- 从读取器或文档加载Wml
结构ToString
- 将Wml
结构保存到string
“Equals
”和“Clone
”等功能是由于存在一个可以逐节点遍历 DOM 和 IWmlSerializable
结构的 WmlNodeReader
和 WmlNodeWriter
而产生的副作用。在 .NET XML 序列化中,除非首先生成完整的中间 XML 输出,否则无法实现这一点。将差异文档合并到现有的 IWmlSerializable
实例或 Wml
DOM 中只会添加、删除或更新任何差异;数据结构的其他部分不会更改。
事务
能够计算前后状态之间的差异的主要好处之一是,您可以撤销(或重做)您对数据对象所做的任何操作。唯一的限制是更改必须使数据结构保持有效的可序列化状态,这主要意味着您必须确保在将对象添加到现有数据结构之前,该对象具有有效的标识或 GetHashCode()
结果。
可以使用 WmlSerializer
类中的不同方法(例如 Serialize
、Compare
和 Combine
)手动来回滚动对象的状态,但 Wml
库通过 WmlTransaction
类使 IWmlSerializable
实例上的操作更简单。
例如
WmlTransaction transaction = new WmlTransaction (myDataObject,
"my transaction name");
try
{
// throws a validation exception if any changes are bad
myDataObject.MakeChanges();
// changes are committed and differences recorded
transaction.Commit();
}
catch (Exception)
{
// changes are rolled back and differences discarded
transaction.RollBack();
}
创建时,transaction
对象会将 IWmlSerializable
序列化到 WmlDocument
中,以保存“先前状态”。当事务完成时,将计算对数据对象的差异,并用于在调用“RollBack
”时回滚数据对象(然后立即丢弃),或在调用“Commit
”时存储以提供差异历史记录。
使用单个事务效率不高,因为 WmlTransaction
在实例化时需要序列化数据对象先前状态的副本。如果您对数据执行多个事务,更好的技术是使用 WmlTransactionLog
类,该类将缓存并自动更新任何更改上的先前状态,同时还会保留已提交事务的副本,您可以来回滚动这些副本以实现撤销/重做。
例如
WmlTransactionLog transactionLog = new WmlTransactionLog();
transactionLog.CurrentState = myDataObject;
WmlTransaction transaction1 =
transactionLog.BeginTransaction ("Change 1");
myDataObject.MakeChanges();
WmlTransaction transaction1a =
transactionLog.BeginTransaction ("Change 1 a");
myDataObject.MakeChanges();
transaction1a.RollBack();
transaction1.Commit();
WmlTransaction transaction2 =
transactionLog.BeginTransaction ("Change 2");
myDataObject.MakeChanges();
transaction2.Commit();
// Roll back our committed transactions to the
// starting state
transactionLog.RollBack();
transactionLog.RollBack();
// Roll forward our committed transactions to the
// finish state
transactionLog.RollForward();
transactionLog.RollForward();
如本示例所示,事务可以嵌套,前提是它们以相反的顺序回滚或提交。但是,嵌套事务的成本更高,因为必须计算新的“先前状态”,就像事务日志缓存的版本无法使用一样。事务日志还有一个“Modified
”事件,该事件在当前状态更改时(例如,当事务提交时)引发,这对于知道何时刷新用户界面非常有用。
演示应用程序
示例应用程序演示了如何使之前示例中使用的简单类“JobPosition
”成为 Wml
可序列化。演示用户界面只允许您对其执行一项操作,“Modify
”,该操作会随机打乱数据,向父集合和“DirectReports
”引用添加和删除 JobPosition
s,并修改描述性信息。
它演示了以下 Wml
序列化功能
- 使用
WmlSerializer
将IWmlSerializable
数据结构加载(反序列化)和保存(序列化)到文件。 - 维护一个
WmlTransactionLog
实例,以提供撤销/重做功能,并在数据对象被修改时通知用户界面。 - 使用“
WmlSerializer.Equals()
”将数据对象与其在事务日志中存储的先前状态进行比较(因为随机化操作并不总是会更改数据结构)。
所有这一切只需很少的代码...
结论
Wml
库并不是为了取代 .NET XML 序列化而编写的。它不是那么快、那么灵活或那么健壮(抱歉)。对于其预期用途,即跟踪数据结构的变化,它甚至可能不如自定义解决方案好,因为每次“Compare
”差异比较都必须遍历整个数据结构。手动构建更改日志可能更有效率。此外,对于在每次操作中状态变化很大一部分的任何数据,保留差异状态是适得其反的。
然而,Wml
库在程序员的支持非常少的情况下完成了它的工作。任何当前 XML 可序列化的类型都可以毫不费力地使其能够被 Wml
序列化。
我发现它还有其他一些用途
- 深层复制和相等性测试。
- 通过仅发送自上次服务器更新以来发生的更改来保持智能客户端同步。
- 允许将验证代码放在数据结构本身中,而不是在更改应用之前预先验证它们,因为如果存在任何验证错误,则可以回滚。
- 应用程序中的撤销/重做功能。
- 构建可以安全地操作主数据对象的、多层模态对话框(因为主数据对象可以回滚更改,或者将克隆结构上的更改合并进来)。
- 更改日志。
我在 .NET Framework 1.1 版本上开发了 Wml
库,并且尚未在 .NET Framework 2.0 版本中对其进行泛型类型的测试。我不认为会有任何问题,只要泛型类型的反射即使在非泛型类型上也表现一致,尽管我无法确认这一点。
致谢
- Chris Beckett,感谢他关于使用他漂亮的自定义扩展类来为演示中的菜单图像的文章和代码。
- Marc Clifton,感谢他关于简单序列化器/反序列化的文章,感谢他建议我使用
TypeConverter
(我以前曾笨拙地使用反射来查找基本类型的ToString()
方法和static Parse()
方法)。
历史
- 2006 年 1 月 26 日 - 版本 1.0
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。