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()] |
有了刚才打下的基础,我现在就可以开始讨论如何实现撤销功能了。这涉及三种操作:为对象的状态拍快照,在撤销的时候恢复对象的状态,以及在接受操作的时候保存对象的状态。
此外,如果对象中含有实现了Csla.Core.IUndoableObject接口的子对象的话,这些子对象必须也要执行储备、恢复和接受这些操作。为了实现这些操作,每当算法遇到从这些类型中的任意一种继承过来的域的时候,将会对该对象迭代地执行这些操作,以便进行正确地处理。
这三种操作将会由三个方法来实现:
CopyState() |
3.2.10.1 CopyState
CopyState()方法会为当前对象的数据拍一张快照,并将它保存在一个System.Collections.Stack对象中。
3.2.10.2 数据进栈
因为这是实现N层撤销的功能,每个对象都要存储一些快照。每次当执行撤销或者接受操作的时候,都会除去所保存的最近的那张快照;这也是“栈”这种数据结构的经典动作。幸运的是,.NET框架内建了Stack<T>这个类,实现了所需要的功能。这个类的声明如下:
[NotUndoable()] |
拍摄数据快照
拍摄某个对象中每个域数据快照的过程是有点儿棘手的。这里使用了反射来遍历对象所有的域。在这个过程中,每个域都被检查看看有没有[NotUndoable()]特性。如果有的话,这个域就被忽略了。
严重的问题在于域的名字在对象中可能不是唯一的。看看下面这两个类,你就明白我的意思了:
public class BaseClass |
这里,每个类都有一个叫做_id的域——在大多数情况下,这没有问题。然而,在使用反射来在一个SubClass对象中遍历所有的域的时候,它会返回两个_id域:在这个继承层次中的每个类都有一个。
为了得到某个对象数据的精确快照,CopyState()需要妥善地处理这种情况。在实践中,这意味着在每个域名字的前面都加上它所属的类的名字作为前缀。这样就得到了BaseClass!_id和SubClass!_id,而不是两个_id域。用感叹号作为分隔符号是随意选择的,但是确实需要某个字符来将类名字和域名字分隔开。
好像还嫌这不够复杂似的,反射对于那些从与自身位于同一个程序集中的父类继承来的类,和对于那些从与自身位于不同程序集中的父类继承来的类的处理方法是不一样的。
如果在上面的例子中,BaseClass和SubClass位于同一个程序集,则只需要一种技术,但是如果它们位于不同程序集,则需要不同的技术。当然,CopyState()对这两种情况都应该能处理,所以业务开发人员无须关心这些细节。
注意:本书没有列出UndoableBase中的所有代码。我只涉及了算法中的关键部分。对于代码的其他部分,请参考下载的代码。
下面的方法处理了前面所讲的所有问题。我会在后面解释它是如何工作的。
[EditorBrowsable(EditorBrowsableState.Never)] |
这个方法的作用域是protected internal的,这不是通常的做法。这个方法需要protected的作用域是因为BusinessBase会继承UndoableBase,而且BusinessBase中的BeginEdit()方法需要调用CopyState()。这部分还是相当简单的。
然而,这个方法还需要internal的作用域,因为子业务对象是被包含在业务集合中的。当某个集合需要拍自身数据的快照的时候,集合中的每个对象也都需要对自己的数据拍快照。BusinessListBase的代码会遍历所有它包含的业务对象,通知每个业务对象都拍一张自身状态的快照。这个动作是通过CopyState()方法完成的,就是说BusinessListBase也需要有能够调用这个方法的能力。因为它是在同一个项目中的,所以这是用internal作用域来完成的。
要拍摄数据的快照,需要在把不同的域值放入栈中之前找到一个地方来存储它们。选择HybridDictionary来做这件事情非常理想,因为它能存储键/值型的数据。它还能根据键名高速地存取所对应的值,这对于实现撤销功能来说非常重要。最后,HybridDictionary对象支持.NET的序列化,这就是说它可以被序列化,并作为业务对象的一部分按值在网络上传递。
CopyState()程序基本上会是一个巨大的循环,从对象继承层次中最外层的类开始,在类的链条上倒退回来,直到遇到UndoableBase。在这个时候,它就可以停下来了——它知道已经得到了所有业务数据的快照。
在这个方法的最后,它调用了CopyStateComplete()。请注意CopyStateComplete()是一个没有实现的virtual方法。设计这个方法的目的是,在拷贝了对象的状态后,如果需要做一些额外的动作的话,那么子类可以重载这个方法。虽然框架本身并没有重载这个方法,但是这个方法为高级的业务开发人员扩展框架提供了可能。
域列表的获得
所有实际的动作都发生在那个循环中。而这一切的第一步就是获得当前类的所有相关域的列表:
// 获取这个类型中所有域的列表 |
避免域的重复处理
前面讨论过,FieldInfo数组有可能包含有当前类的基类中的域。由于Just-in-Time(JIT)编译器在同一个程序集中优化代码的方式,如果某些基类与该子业务类位于同一个程序集中,同一个域的名字有可能会在多个类中列出!在代码遍历该继承层次的时候,可能会将这些域处理两次。
为了避免这种情况,代码应该只处理那些直接从属于当前被处理的类的域:
foreach(FieldInfo field in fields) |
跳过[NotUndoable()]域
经过刚才的处理,我们已经使当前的FieldInfo对象代表了继承层次中当前类对象的某一个域。然而,只有不带有[NotUndoable()]特性的域才应该拍快照:
// 看一看这个域是不是被标记为不可操作 |
对子对象或集合的迭代调用
如果要处理的域是一个指向一个Csla.Core.IUNdoableObject对象的引用,那么CopyState()调用必须要迭代进该对象,这样才能获得完整的快照:
if |
如果一个对象要“深入”另外一个对象,并对其状态进行操纵,这就违背了封装。应该由那个另外的对象来管理它自己的状态。通过将CopyState()调用迭代到子对象中,就可以让该子对象来拍摄它自身状态的快照。请记住,如果子对象是从BusinessListBase继承而来的话,那么调用将会自动地向下迭代到集合的每一个子对象中。
技巧:当然,GetValue()方法所有的返回值都是object类型的,所以需要把结果转换成Csla.Core.IEditableObject类型,这样才能调用相应的方法。
然后,用来撤销或者接受修改的方法会按照相同的方法工作——就是说,它们都会将调用迭代到子对象中。用这种方法,所有的对象就都可以在不违背封装的情况下实现撤销功能了。
为普通域拍快照
对于一个普通的域,代码简单地将域中的值保存在一个Hashtable对象中,把值和类名字与域名字的组合关联在一起:
// 这是一个正常的域,简单的获取该值 |
在遍历了对象继承层次中的每一个类中的每一个域之后,Hashtable就包含了关于该业务对象中所有数据的一个复杂的快照。
注意:这个快照将包含一些被置于BusinessBase类中的域,这些域用来跟踪该对象的状态(如对象是否是新建的、含有脏数据的或已经被删除等)。这个快照还会包含留在以后实现的失效规则的集合。这所有的数据在执行撤销操作的时候都会将对象恢复到原来的状态。
Hashtable的序列化和进栈
现在,我们已经记录了对象所有域的值,但是快照是存放在一种复杂的数据类型Hashtable中的。使事情更复杂的是,包含在Hashtable中的某些元素有可能是指向更复杂对象的引用的。在这种情况下,Hashtable仅仅包含了对现存对象的引用,根本不是对这个对象的拷贝或是快照。
幸运的是,这两个问题都可以简单地得到解决。BinaryFormatter可以用来将Hashtable转换成字节流,从复杂的数据类型转换成了一种非常便于存储的类型。然而更好的是,Hashtable的这个序列化过程可以自动地序列化任何它所引用的对象。
这样做一定需要任何业务对象所引用的所有的对象都必须是标记成[Serializable()]的,以便可以把它们添加到字节流中。如果被引用的对象不是可序列化的,那么序列化过程就会造成运行时错误。你也可以将所有不能序列化的对象都标记成是[NotUndoable()]的,这样撤销过程就会简单地忽略它们。
执行序列化的代码相当简单:
// 对状态进行序列化并且将它保存在堆栈中 |
BinaryFormatter对象将Hashtable(和它所引用的任何对象)序列化到一个内存缓冲区内的字节流中。字节流随后被简单地从内存缓冲区中提取出来,压进栈中:
stateStack.Push(buffer.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; |
这是前面将Hashtable压进堆栈那个过程的反操作。操作最后得到了包含有被拍成原始快照的数据的Hashtable。
恢复对象的状态数据
有了装有原始对象值的Hashtable,我们就可以用CopyState()中的相同方法来遍历对象中的所有域。
如果代码遇到实现了Csla.Core.IEditableObject接口的子业务对象,它就会将UndoChanges()迭代到该子对象中,以便每个对象都能完成自身的恢复操作。同样,这么做是为了保留封装性——只有给定对象内部的代码才能操作该对象的数据。
对于一个“普通”的域来说,域值被简单地从Hashtable恢复回来了:
// 这个一个常规的域,对它的数值进行恢复 |
3.2.10.4 AcceptChanges
AcceptChanges()实际上是这三个方法中最简单地那个。如果做出的修改得到接受,就意味着对象当前的值应该被保留下来,而且最近的快照就变得没有意义,可以被丢弃掉了。与CopyStat()一样,一旦这个方法执行结束,会调用一个AcceptChangesComplete()方法来允许子类来做一些额外的动作。
从概念上讲,AcceptChanges()需要做的全部工作就是丢弃最近的快照:
_stateStack.Pop(); |
// 这个域不可操作,因此看一看它是不是一个子对象 |
简单域的值不需要任何处理。请记住,在这里当前的值已经被接受了——所以根本无须对当前的值再作任何修改。
| 回书目 |
|
· Linux服务器架设自测获.. · 边界网关安全防护自测.. · Cisco CCNA最新真题自.. · 我在美联储监管银行 书.. · 我在美联储监管银行 目.. · 我在美联储监管银行 前.. |
· 入侵的艺术 目录 · 入侵的艺术 前言 · 网管员全真面试题自测.. · 子弹的本质—— 形势没.. · 学习大量的词汇—— 对.. · 重用的情况怎样 |
|
|||
| · 病毒查杀专题 · 802.11n:下一代的无线.. · 反垃圾邮件技术应用 · 运营商封堵非法ADSL共享 · Windows Home Server .. · Windows Server 2008 · Sun以10亿美元并购开源.. · VoIP的安全性探讨 |
· 甲骨文85亿美元收购BEA · 如何优化IT 控制能耗 · 龙芯要做中国的“奔腾” · 手机病毒揭密 · 清除流氓软件——51CTO.. · 华为、贝恩资本22亿美.. · 网络安全之网吧安全 · 数字证书技术ABC |
||
|
|||
| · VPN技术 · SOA 面向服务架构 · 子网掩码教程 · 三层交换技术专题 · Windows远程桌面应用 · 深入了解PGP加密技术 · MySQL数据库备份 · 病毒查杀专题 |
· VPN技术 · Solaris 10 配置管理 · Linux 基础 · Linux防火墙 · SSL VPN详细知识 · 路由器设置与口令恢复 · 打造安全服务器 · Linux 集群技术专题 |
||
|
|||
| · VPN技术 · SOA 面向服务架构 · 子网掩码教程 · 三层交换技术专题 · Windows远程桌面应用 · MySQL数据库备份 · 身份认证技术 · 病毒查杀专题 |
· 清除流氓软件——51CTO.. · SSL VPN详细知识 · Sniffer安全技术从入门.. · 常用交换机典型配置 · Linux 集群技术专题 · VPN技术 · 路由器设置与口令恢复 · Linux 基础 |
||
| ·DB2 Viper快速入门 ·DB2 9数据库的镜像分割与.. |
·将XML应用程序从DB2 8.x.. ·DB2 9中的pureXML:如何.. |
| ·ASP.NET 2.0 Web Part编.. ·ASP.NET 2.0 Web Part编.. |
·ASP.NET 2.0 Web Part编.. ·ASP.NET 2.0服务器控件之.. |
| ·在VM6中做Windows Server.. ·让服务器自动启动失败的E.. |
·使用ISA Server保护内部.. ·使用WUCDCreator将SCSI、.. |
| ·这才叫电脑高手! ·网络安全中的“秘语”VS .. |
·三转CHM文件故障解决 ·错误是可以避免的——系.. |
| · 思科警告统一通信管理存.. · 家用路由器存在严重缺陷.. · 华为2008市场攻略 240亿.. |
· 浅谈国内的渗透评估过程 · VPN安全技术与应用 · 企业如何进行计算机取证.. |
| · IDC:2008年IT市场10大.. · Visual Studio 2005开发.. · 测试开发人员参考手册 |
· 年初17大热门技术 年底.. · 解析Ajax开发框架 走进A.. · 基于Google Maps与Ajax.. |
| · Linux操作系统下文件和.. · 热点:国内大型企业如此.. · IBM和Sun起争议 坚持不.. |
· 中小企业刀片市场将达20.. · IT人员应当了解的七个存.. · IDC:2008年IT市场10大.. |
| · 甲骨文Oracle 11g正式发.. · Oracle数据库开发之PL/S.. · Oracle数据库开发基础教.. |
· IT人员应当了解的七个存.. · 希捷承认部分硬盘暗藏病.. · 硬盘之父获得诺贝尔物理.. |