频 道 直 达 - 新闻 - 培训 - 软件 - 教程 - 前沿 - 组网 - 系统应用 - 安全 - 编程 - 存储 - 操作系统 - 数据库 - 服务器 - 专题 - 产品 - 案例库 - 读书 - 博客 - BBS
51CTO.COM_中国最大的网络技术网站
找资料:

3.2.10 UndoableBase类

作者: (美)霍特卡(Lhotka,R.)著;王鑫译 出处:电子工业出版社博文视点  (  ) 砖  (  ) 好  评论 ( ) 条  进入论坛
更新时间:2008-01-24 12:09
关 键 词:UndoableBase    业务框架  Expert C# 2005 Business Objects中文版
阅读提示:《Expert C# 2005 Business Objects中文版》第3章介绍了创建类,以及所有的工具类及其功能。本文是UndoableBase类。

3.2.10  UndoableBase类

UndoableBase类包含有处理某个对象的N层撤销的所有的具体实现。这是相当复杂的代码,使用了大量的反射来找出每个业务对象中的所有域,为域中的值拍快照,然后(可能会)在用户撤销的时候恢复原值。

请记住,不是所有的东西都需要N层撤销。在很多Web环境下,如第10章所描述的,根本就不需要使用这些方法。一个没有取消按钮的简单UI不需要撤销功能,所以也就没必要花费额外的开销来为对象的数据拍快照。另一方面,在创建包含能编辑子对象(或甚至子对象的子对象)

的模式对话框窗体的复杂Windows Forms UI的时候,就最好调用这些方法来支持在对话框窗体上的OK和取消按钮。
技巧:通常来说,应该在用户或者应用程序允许与业务对象交互之前来拍取该对象域的快照。这样,你就总可以将对象撤销回原来初始的状态。BusinessBase和BusinessListBase类都包含有触发拍快照过程的BeginEdit()方法、将对象恢复到上一张快照状态的CancelEdit()方法和提交自上一张快照以来所有修改的ApplyEdit()方法。

拍快照过程如此复杂的原因在于它要拷贝每个对象中的所有域里面的值,而且每个业务对象都基本上会包含多个通过继承和聚合交织在一起的类。这就会在父类和子类拥有相同名字的域的时候造成问题,尤其是在父类与子类不在同一个程序集中的时候。

因为这是Csla.BusinessBase最终会继承的基类,所以它必须要被标记为是[Serializable()]的。它还应该被声明为是abstract的,以便没有人能够直接从这个类创建实例。所有的业务对象都需要应用在BindableBase中实现的INotifyPropertyChanged接口,所以它们也要从这个类继承。最后,N层撤销功能依赖于Csla.Core.IUndoableObject接口,所以这个功能也将会在这个类中(和本章后面的BusinessListBase类中)来实现:

[Serializable()]
public abstract class UndoableBase : Csla.Core.BindableBase,
Csla.Core.IUndoableObject
{
}

有了刚才打下的基础,我现在就可以开始讨论如何实现撤销功能了。这涉及三种操作:为对象的状态拍快照,在撤销的时候恢复对象的状态,以及在接受操作的时候保存对象的状态。

此外,如果对象中含有实现了Csla.Core.IUndoableObject接口的子对象的话,这些子对象必须也要执行储备、恢复和接受这些操作。为了实现这些操作,每当算法遇到从这些类型中的任意一种继承过来的域的时候,将会对该对象迭代地执行这些操作,以便进行正确地处理。

这三种操作将会由三个方法来实现:

CopyState()
 UndoChanges()
 AcceptChanges()

3.2.10.1  CopyState

CopyState()方法会为当前对象的数据拍一张快照,并将它保存在一个System.Collections.Stack对象中。

3.2.10.2  数据进栈

因为这是实现N层撤销的功能,每个对象都要存储一些快照。每次当执行撤销或者接受操作的时候,都会除去所保存的最近的那张快照;这也是“栈”这种数据结构的经典动作。幸运的是,.NET框架内建了Stack<T>这个类,实现了所需要的功能。这个类的声明如下:

[NotUndoable()]
private Stack<byte[]> _stateStack = new Stack<byte[]>();
这个域被标记为是[NotUndoable()]的,这可以避免重复地对以往的快照再拍快照。CopyState()应该只是简单地把包含实际业务数据的域记录下来。一旦拍摄了对象数据的快照,该快照就会被序列化到一个字节流中。然后这个字节流就会被放到栈上。从那里,如果需要的话它可以被提取出来,并被反序列化来执行撤销的操作。

拍摄数据快照

拍摄某个对象中每个域数据快照的过程是有点儿棘手的。这里使用了反射来遍历对象所有的域。在这个过程中,每个域都被检查看看有没有[NotUndoable()]特性。如果有的话,这个域就被忽略了。

严重的问题在于域的名字在对象中可能不是唯一的。看看下面这两个类,你就明白我的意思了:

public class BaseClass
{
int _id;
}
public class SubClass : BaseClass
{
int _id;
}

这里,每个类都有一个叫做_id的域——在大多数情况下,这没有问题。然而,在使用反射来在一个SubClass对象中遍历所有的域的时候,它会返回两个_id域:在这个继承层次中的每个类都有一个。

为了得到某个对象数据的精确快照,CopyState()需要妥善地处理这种情况。在实践中,这意味着在每个域名字的前面都加上它所属的类的名字作为前缀。这样就得到了BaseClass!_id和SubClass!_id,而不是两个_id域。用感叹号作为分隔符号是随意选择的,但是确实需要某个字符来将类名字和域名字分隔开。

好像还嫌这不够复杂似的,反射对于那些从与自身位于同一个程序集中的父类继承来的类,和对于那些从与自身位于不同程序集中的父类继承来的类的处理方法是不一样的。

如果在上面的例子中,BaseClass和SubClass位于同一个程序集,则只需要一种技术,但是如果它们位于不同程序集,则需要不同的技术。当然,CopyState()对这两种情况都应该能处理,所以业务开发人员无须关心这些细节。

注意:本书没有列出UndoableBase中的所有代码。我只涉及了算法中的关键部分。对于代码的其他部分,请参考下载的代码。

下面的方法处理了前面所讲的所有问题。我会在后面解释它是如何工作的。

[EditorBrowsable(EditorBrowsableState.Never)]
protected internal void CopyState()
{
Type currentType = this.GetType();
HybridDictionary state = new HybridDictionary();
FieldInfo[] fields;
string fieldName;
do
{
// 获取这个类型中所有域的列表
fields = currentType.GetFields(
BindingFlags.NonPublic |
BindingFlags.Instance |
BindingFlags.Public);

foreach (FieldInfo field in fields)
{
// 确保我们只处理我们自己的变量
if (field.DeclaringType == currentType)
{
// 看一看这个域是否被标记为不可操作
if (!NotUndoableField(field))
{
// 这个域不可操作,因此我们需要对它进行处理
object value = field.GetValue(this);

if (typeof(
Csla.Core.IUndoableObject).IsAssignableFrom(
field.FieldType))
{
// 确保这个变量拥有一个值
if (value != null)
{
//这是一个子对象,对这个调用进行递归
((Core.IEditableObject)value).CopyState();
}
}
else
{
// 这是一个正常的域,简单的获取该值
fieldName =
field.DeclaringType.Name + "!" + field.Name;
state.Add(fieldName, value);
}
}
}
}
currentType = currentType.BaseType;
} while (currentType != typeof(UndoableBase));

// 对状态进行序列化并且存储在堆栈中
using (MemoryStream buffer = new MemoryStream())
{

BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(buffer, state);
_stateStack.Push(buffer.ToArray());
}
CopyStateComplete();
}

protected virtual void CopyStateComplete()
{
}

这个方法的作用域是protected internal的,这不是通常的做法。这个方法需要protected的作用域是因为BusinessBase会继承UndoableBase,而且BusinessBase中的BeginEdit()方法需要调用CopyState()。这部分还是相当简单的。

然而,这个方法还需要internal的作用域,因为子业务对象是被包含在业务集合中的。当某个集合需要拍自身数据的快照的时候,集合中的每个对象也都需要对自己的数据拍快照。BusinessListBase的代码会遍历所有它包含的业务对象,通知每个业务对象都拍一张自身状态的快照。这个动作是通过CopyState()方法完成的,就是说BusinessListBase也需要有能够调用这个方法的能力。因为它是在同一个项目中的,所以这是用internal作用域来完成的。

要拍摄数据的快照,需要在把不同的域值放入栈中之前找到一个地方来存储它们。选择HybridDictionary来做这件事情非常理想,因为它能存储键/值型的数据。它还能根据键名高速地存取所对应的值,这对于实现撤销功能来说非常重要。最后,HybridDictionary对象支持.NET的序列化,这就是说它可以被序列化,并作为业务对象的一部分按值在网络上传递。

CopyState()程序基本上会是一个巨大的循环,从对象继承层次中最外层的类开始,在类的链条上倒退回来,直到遇到UndoableBase。在这个时候,它就可以停下来了——它知道已经得到了所有业务数据的快照。

在这个方法的最后,它调用了CopyStateComplete()。请注意CopyStateComplete()是一个没有实现的virtual方法。设计这个方法的目的是,在拷贝了对象的状态后,如果需要做一些额外的动作的话,那么子类可以重载这个方法。虽然框架本身并没有重载这个方法,但是这个方法为高级的业务开发人员扩展框架提供了可能。

域列表的获得

所有实际的动作都发生在那个循环中。而这一切的第一步就是获得当前类的所有相关域的列表:

// 获取这个类型中所有域的列表
fields = currentType.GetFields(
BindingFlags.NonPublic |
BindingFlags.Instance |
BindingFlags.Public);
域是不是public的没有关系——与作用域没有关系,它们都需要被记录下来。更重要的是只需要记录那些实例域,而不是那些被声明为static的域。调用的结果是得到了一个FieldInfo对象的数组,数组中每一个对象都对应着业务对象中的一个域。

避免域的重复处理

前面讨论过,FieldInfo数组有可能包含有当前类的基类中的域。由于Just-in-Time(JIT)编译器在同一个程序集中优化代码的方式,如果某些基类与该子业务类位于同一个程序集中,同一个域的名字有可能会在多个类中列出!在代码遍历该继承层次的时候,可能会将这些域处理两次。

为了避免这种情况,代码应该只处理那些直接从属于当前被处理的类的域:

foreach(FieldInfo field in fields)
{
// 确保我们只处理我们自己的变量
if(field.DeclaringType == currentType)

跳过[NotUndoable()]域

经过刚才的处理,我们已经使当前的FieldInfo对象代表了继承层次中当前类对象的某一个域。然而,只有不带有[NotUndoable()]特性的域才应该拍快照:

// 看一看这个域是不是被标记为不可操作
if(!NotUndoableField(field))
到了这个时候,我们很清楚这个域的值应该是所摄快照的一部分,所以有两种可能:这是一个普通的域,或者这是指向一个实现了Csla.Core.IUndoableObject接口的子对象的引用。

对子对象或集合的迭代调用

如果要处理的域是一个指向一个Csla.Core.IUNdoableObject对象的引用,那么CopyState()调用必须要迭代进该对象,这样才能获得完整的快照:

if
(typeof(Csla.Core.IUndoableObject).
IsAssignableFrom(field.FieldType))
{
// 确保这个变量拥有一个值
if (value != null)
{
// 这是一个子对象,对这个调用进行递归操作
((Core.IUndoableObject)value).CopyState();
}
}

如果一个对象要“深入”另外一个对象,并对其状态进行操纵,这就违背了封装。应该由那个另外的对象来管理它自己的状态。通过将CopyState()调用迭代到子对象中,就可以让该子对象来拍摄它自身状态的快照。请记住,如果子对象是从BusinessListBase继承而来的话,那么调用将会自动地向下迭代到集合的每一个子对象中。

技巧:当然,GetValue()方法所有的返回值都是object类型的,所以需要把结果转换成Csla.Core.IEditableObject类型,这样才能调用相应的方法。

然后,用来撤销或者接受修改的方法会按照相同的方法工作——就是说,它们都会将调用迭代到子对象中。用这种方法,所有的对象就都可以在不违背封装的情况下实现撤销功能了。

为普通域拍快照

对于一个普通的域,代码简单地将域中的值保存在一个Hashtable对象中,把值和类名字与域名字的组合关联在一起:

// 这是一个正常的域,简单的获取该值
fieldName = field.DeclaringType.Name + "!" + field.Name;
state.Add(fieldName, value);
请注意,这些“普通”的域其中可能实际上包含有很复杂的类型。我们只是知道该域没有引用一个可编辑的业务对象,因为它的值没有实现Csla.Core.IUndoableObject接口。它有可能是像int或string这样的简单类型,也有可能是一个复杂的对象(只要该对象是标记为[Serializable()]的)。

在遍历了对象继承层次中的每一个类中的每一个域之后,Hashtable就包含了关于该业务对象中所有数据的一个复杂的快照。

注意:这个快照将包含一些被置于BusinessBase类中的域,这些域用来跟踪该对象的状态(如对象是否是新建的、含有脏数据的或已经被删除等)。这个快照还会包含留在以后实现的失效规则的集合。这所有的数据在执行撤销操作的时候都会将对象恢复到原来的状态。

Hashtable的序列化和进栈

现在,我们已经记录了对象所有域的值,但是快照是存放在一种复杂的数据类型Hashtable中的。使事情更复杂的是,包含在Hashtable中的某些元素有可能是指向更复杂对象的引用的。在这种情况下,Hashtable仅仅包含了对现存对象的引用,根本不是对这个对象的拷贝或是快照。

幸运的是,这两个问题都可以简单地得到解决。BinaryFormatter可以用来将Hashtable转换成字节流,从复杂的数据类型转换成了一种非常便于存储的类型。然而更好的是,Hashtable的这个序列化过程可以自动地序列化任何它所引用的对象。

这样做一定需要任何业务对象所引用的所有的对象都必须是标记成[Serializable()]的,以便可以把它们添加到字节流中。如果被引用的对象不是可序列化的,那么序列化过程就会造成运行时错误。你也可以将所有不能序列化的对象都标记成是[NotUndoable()]的,这样撤销过程就会简单地忽略它们。

执行序列化的代码相当简单:

// 对状态进行序列化并且将它保存在堆栈中
using (MemoryStream buffer = new MemoryStream())
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(buffer, state);
_stateStack.Push(buffer.ToArray());
}
这段代码与前面在ObjectCloner类中实现的克隆代码非常相似。

BinaryFormatter对象将Hashtable(和它所引用的任何对象)序列化到一个内存缓冲区内的字节流中。字节流随后被简单地从内存缓冲区中提取出来,压进栈中:

stateStack.Push(buffer.ToArray());   
将MemoryStream转换成字节数组不是问题,因为MemoryStream就是在字节数组中来存储数据的。ToArray()方法简单地返回一个对现存数组的引用,所以没有数据拷贝的动作。

然而,这个转换到字节数组的动作非常重要,因为字节数组是可以序列化的,而MemoryStream对象不可以。如果业务对象在被编辑的过程中,需要按值在网络上传递,保存该对象状态的栈就需要是可序列化的。

技巧:没有办法预测在对象被编辑的过程中是否需要将它们在网络上传递,但是因为业务对象都是[Serializable()]的,你无法阻止业务开发人员这么做。如果栈要引用某个MemoryStream的话,业务应用程序将会因为序列化失败而在运行时得到一个错误,而这是不可接受的。将数据转换成字节数组可以避免在业务开发人员确实决定在对象编辑过程中在网络上传递这个对象的时候,使应用程序崩溃。

现在,我们已经完成了实现N层撤销功能的三分之一。现在可以创建一个对象数据快照的堆栈。到了该继续下去,开始讨论撤销和接受操作的时候了。

3.2.10.3  UndoChanges

UndoChanged()方法是CopyState()的一个反操作。它从堆栈上取出数据快照,将快照反序列化到Hashtable中,然后从Hashtable中取出每一个值,将其恢复到合适的对象域中。与CopyState()一样,一旦方法结束,就会调用一个virtual的UndoChangesComplete()方法,来允许子类执行额外的动作。这个方法会稍后在Csla.Core.BusinessBase中被重载。
CopyState()方法的实现解决了在对象继承层次中遍历所有的类型,并找到对象中所有域的这个难题。因此UndoChanges()的结构在本质上会与之一样,除了UndoChanges()会恢复所有的域值,而不是拍摄快照。

因为UndoChanges()整体的结构基本上就是CopyState()的反操作,我在这里就不列出所有的代码了。我会把注意力集中在它的关键功能上。

EditLevel

业务开发人员是有可能在根本就没有状态可保存的时候意外地触发对UndoChanges()的调用的。如果没有满足这个条件,将会造成运行时错误。为了避免这种情况的发生,UndoChanges()要做的第一件事情就是通过提取堆栈对象的Count属性,来获得对象的“修改级别”。如果修改级别为0,那么就说明没有需要保存的状态,UndoChanges()什么都不做,直接退出。

这个修改级别的概念在后面实现BusinessListBase的时候甚至会变得更为重要,所以你会注意到这个值是用一个属性来实现的。

重新创建哈希表对象

在CopyState()处理过程的最后,Hashtable被序列化成了一个字节数组,与之对应的是,UndoChanges()要做的第一件事就是从堆栈弹出最近加入的快照,并将它反序列化来重新生成包含有详细数据的Hashtable对象:

Hashtable state;
using (MemoryStream buffer = new MemoryStream(_stateStack.Pop()))
{
buffer.Position = 0;
BinaryFormatter formatter = new BinaryFormatter();
state = (Hashtable)formatter.Deserialize(buffer);
}

这是前面将Hashtable压进堆栈那个过程的反操作。操作最后得到了包含有被拍成原始快照的数据的Hashtable。
恢复对象的状态数据

有了装有原始对象值的Hashtable,我们就可以用CopyState()中的相同方法来遍历对象中的所有域。
如果代码遇到实现了Csla.Core.IEditableObject接口的子业务对象,它就会将UndoChanges()迭代到该子对象中,以便每个对象都能完成自身的恢复操作。同样,这么做是为了保留封装性——只有给定对象内部的代码才能操作该对象的数据。

对于一个“普通”的域来说,域值被简单地从Hashtable恢复回来了:

// 这个一个常规的域,对它的数值进行恢复
fieldName = field.DeclaringType.Name + "!" + field.Name;
field.SetValue(this, state[fieldName]);
在这个处理过程的最后,对象将被重置到它上一次拍快照那个时候的状态。现在就只剩下实现如何接受修改的方法了,撤销的部分就完成了。

3.2.10.4  AcceptChanges

AcceptChanges()实际上是这三个方法中最简单地那个。如果做出的修改得到接受,就意味着对象当前的值应该被保留下来,而且最近的快照就变得没有意义,可以被丢弃掉了。与CopyStat()一样,一旦这个方法执行结束,会调用一个AcceptChangesComplete()方法来允许子类来做一些额外的动作。

从概念上讲,AcceptChanges()需要做的全部工作就是丢弃最近的快照:

_stateStack.Pop();  
然而,请记住对象可能含有子对象,它们也需要知道接受所做的修改。这就需要遍历对象所有的域,来找到所有实现了Csla.Core.IUndoableObject接口的子对象。AcceptChanges()方法也必须对这些子对象进行迭代才行。
遍历对象中所有域的过程与在CopyState()和UndoChanges()中一模一样。唯一的区别在于方法调用进行迭代的位置不同:

// 这个域不可操作,因此看一看它是不是一个子对象
if
(typeof(Csla.Core.IUndoableObject).
IsAssignableFrom(field.FieldType))
{
object value = field.GetValue(this);
// 确保这个变量拥有一个数值
if (value != null)
{
// 这是一个子对象,因此对这个调用进行递归操作
((Core.IUndoableObject)value).AcceptChanges();
}
}

简单域的值不需要任何处理。请记住,在这里当前的值已经被接受了——所以根本无须对当前的值再作任何修改。

【责任编辑:雪花 TEL:(010)68476606】

回书目      
发表
查看
我也说两句

匿名发表

(如果看不清请点击图片进行更换)


中 国 最 大 的 网 络 技 术 网 站 ·
技 术 成 就 梦 想
订阅技术快讯
电子杂志下载
名称:2007路由技术大全
简介:《2007路由技术大全》由51CTO.com网站特别策划制作,该书包括路由器技术、路由器产品、路由器配置、安全设置、路由器故障处理、路由器密码恢复,以及广大网友在实践使用中的心得经验和技巧文章,内容注重实用性,适用于初学者入门,也适合多年从业者提高,是一本实践和理论完
名称:网络安全精品应用黄皮书
简介:《2007精品网络安全黄皮书》包括了9个大类24个小类, 800余篇文章,内容包含了熊猫烧香病毒、DDOS攻击、ARP病等热点问题的介绍及解决方案。从病毒查杀、防范、系统、数据等各方面的安全设置到黑客技术的了解、防范,涉及到了安全应用的全部领域, 由浅至深内容全面。
名称:Vista精品应用黄皮书
简介:《Vista精品应用黄皮书》囊括了Vista的各方面内容。此次的精简版,是将里面的内容做了提取,便于用户下载和使用。内容包含了各种Vista的安装与实施、技巧与解析以及各种Vista相关学习文档和相关软件的安全下载。该电子书是了解和应用Vista人员必备的工具手册,并且也是第一本
关键字阅读
频道精选