由于.NET框架对消息循环机制进行了很好的封装,开发人员不再需要深入的了解Windows事件/消息实现的具体机制,也无需创建复杂的事件结构体和所谓的消息句柄。我们所要做的无非就是——1、使用重载运算符“+=”注册一个事件;2、编写对于该事件的处理方法。(关于C#2.0中事件处理的相关介绍,请参阅我的文章:C#2.0的泛型代理和事件 :以一当百的快感 )
如此简单,以至于习惯了Win32编程的伙计们对此嗤之以鼻,讽之:“我们是开手排挡车的专业选手,你们.NET一族只能玩玩自动档。什么?你们还看《头文字D》?能看懂吗?”…
不理他们!咱们说咱们的。转头前甩给他们一句话:“迂腐!”。如果不解恨,那么在引用一段名师的话:“我们从不乐意改变自己的工作习惯,就像把妻子的照片放在台灯下面一样。然而,当一种新的方法确实能极大的提高我们的工作效率和行动力时,我们干嘛要固执呢?”——够效果了吧?
言归正传。
前几天,我在编写主窗体与子模块的事件通信时,遇到了一个极其堪称郁闷的问题。说这个问题前,我和大家交代一下我的设计思路。
主窗体(frmMain :IParentForm)
事件成员:
public event ParentEventHandler OnUserListCreated;
事件处理方法:
void ToDoOnRequestUserList(object sender, EventArgs e){
//创建DataTable dt
…
This.OnUserListCreated(this, new ParentEventArgs(dt));
}
某一行注册子窗体事件:
frmChild.OnRequestUserList += new EventHandler (ToDoOnRequestUserList);
子窗体(frmChild)
事件成员:
public event EventHandler OnUserListCreated;
事件处理方法:
void ToDoOnRequestUserReturned(object sender, ParentEventArgs e){}
在OnLoad事件处理方法中注册主窗体的事件:
(this.MdiParent as IParentForm). OnUserListCreated += new ParentEventHandler (ToDoOnRequestUserReturned);
主窗体对象为frmMain,它实现了IParentForm接口,该接口定义了事件成员OnUserListCreated(它的EventArgs为自定义的ParentEventArgs)。frmMain对象在某处创建了一个子窗体frmChild,并注册了frmChild的事件OnRequestUserList。
子窗体对象frmChild在载入时(OnLoad方法中)获得frmMain的引用,并注册了frmMain的事件OnUserListCreated。
根据业务逻辑,子窗体运行的某一时刻,用户行为触发了事件OnRequestUserList,此时frmMain将捕获此事件并调用自身的处理方法生成一个被请求的用户列表(DataTable)。然后,frmMain发出了事件OnUserListCreated以提示列表生成完毕,并将刚刚创建的DataTable作为ParentEventArgs参数插入事件中。随后,子窗体将接收到这个事件,并在自己的事件处理方法中对传来的DataTable进行自己的业务逻辑动作。
在接下来程序的运行中,可爱的代码心情愉悦地顺利执行…但是,好景不长!
当我将打开的子窗体关闭后再重新打开,主窗体在触发OnUserListCreated事件后发生调用目标异常,子窗体在该事件的处理方法中也抛出NullReferenceException异常(未将对象引用设置到对象实例)。当我在子窗体的事件处理方法ToDoOnRequestUserReturned中设置断点调试后发现:所有的控件、变量都为null!!
那叫郁闷,那叫惆怅…公车上、步行中、如厕时、入睡前,我估摸着这种灵异现象可能与最近隔壁邻居家小猫的突然消失有着千丝万缕的联系…当然,作为基督教徒的我,也后怕这是主,耶稣基督对于我大前天横闯马路的惩罚…
无助中,我极其盲目的在frmChild的ToDoOnRequestUserReturned方法中加入了一行语句:“MessageBox.ShowDialog(“So boring a thing!”)”以发泄心情。保存、编译、运行——大坏蛋的面目露了出来!当我第一次打开子窗体的时候,如我所料,程序正常运行并弹出了MessageBox。关键是,当我关闭子窗口并第二次打开它执行时,MessageBox弹出了两次!恩…
带着疑问,我重复了以上关闭、打开步骤,MessageBox弹出了三次!——事情已经有了眉目。在我辗转反复的思考后(也许有人会骂我菜鸟…),终于明白了所有事情的缘由:
因为程序一直处在运行中,所以主窗体对象一直驻留内存中并保持着自身的状态(它没有的disposed),所以,每次子窗体创建时,主窗体都会注册它的OnRequestUserList事件,同样的,该子窗体在加载时,自身也会把主窗体的OnUserListCreated事件注册一次。
问题就出在这里,虽然子窗体关闭了,并disposed了。但是,它关闭时并没有把在主窗体注册的事件同时注销。随着子窗体一次次的打开,主窗体的OnUserListCreated就被+=了N多了注册用户,其中的N-1个用户其实早已经不存在了,而主窗体全然不知。所以当发出OnUserListCreated事件后,主窗体还会以无反顾地去调用这N多个方法代理,这必然会导致异常抛出——唯一打开的那个子窗体接受到一次次传来的事件,并企图调用ToDoOnUserListReturned方法,如果此方法中包含着对本对象成员变量的操作,自然会引出“未将引用设置到对象实例”的异常。
也许有朋友会问,为什么主窗体调用那些早已disposed的frmChild的方法的代理时,会被当前存在的那个frmChild执行呢?我认为这可能是由于类实例的同一个方法在内存栈中共享空间造成的;而成员变量在堆中存放,各自维护其状态,当其所属的对象被释放回收时,其值也就置为null了。(个人观点,望兄弟姐们给予指正)
综上,我做一下总结:
子窗体在关闭时,应当把自己注册的主窗体对象(或者是长久驻留内存对象)事件一一注销。例如本例中,应在子窗体的OnClosed事件处理方法中加入以下代码:
(this.MdiParent as IParentForm). OnUserListCreated -= new ParentEventHandler (ToDoOnRequestUserReturned)
如果仅仅是为了在主窗体执行完某项操作后触发子窗体某一方法的执行,我们通常不采用事件机制,而采用以下两种方法:
A. 将此方法访问属性改为public,然后由主窗体适时调用。
B. 定义一个接口,子窗体对象实现这个接口,并把该目标方法提升为该接口的成员。由主窗体适时调用这个接口成员方法。
愚人节刚过,希望亲爱的programmer们没被下药 J