构建 Silverlight 企业应用程序时的冒险 - 第 35 部分
我在重构框架时遇到的一个问题 - 它涉及到在使用事件等时的代码执行顺序
在这篇文章中,我们将探讨我在重构框架时遇到的问题。它涉及使用事件等时代码的执行顺序。
情况
我们目前正努力按时完成我们的第一个可交付成果。在我们可以真正开始正确测试这段代码之前,需要解决的一个问题是清理积压的问题。我决定自己做这件事,因为这让我更好地了解了常见问题以及我们团队的工作质量。此外,我意识到问题可能由于框架架构中的缺陷而出现,我们不得不采取捷径来实现我们的第一个可交付成果,我希望能够根据需要解决这些问题。
一些架构见解
为了让你理解我为了解决遇到的一个问题所做的事情,重要的是要了解我们的应用程序是如何工作的(至少在客户端)。我们在 Silverlight 客户端中托管了几个“应用程序”。最后,我们可能会有大约六个这样的应用程序。每个应用程序都遵循相同的设计。左侧是导航,由一个“菜单”和一些用于选择记录的 UI 组成。根据用户选择的记录类型以及用户拥有的任何授权,我们加载模块来处理数据。
为了创建新记录,我们使用一个我们称之为任务的概念。这基本上是一个向导式的 UI,它作为弹出窗口在其他 UI 之上运行。它由与用户导航到记录时使用的相同的模块组成,只是处于不同的视觉状态。这会产生一些有趣的场景,因为很可能存在同一模块的两个实例在使用和更新仅具有单个实例的资源。
其中一个场景是在通过任务创建记录后更新导航。由于任务不了解模块做什么,因此模块有责任在创建新记录后更新导航。为此,它更新了我们命名为 ViewState
的东西,它反过来又更新了导航和呈现此新记录给用户所需的模块。
但是,创建记录通常在任务的第一个模块中完成,其他模块才能保存它们的数据(因为它们可能需要与新记录建立关系)。这意味着每当模块收到加载新数据的信号时,大多数模块都无法加载,因为数据尚未存在。这与事物的执行顺序有关,特别是由于我们使用事件直接更新我们的模块。
设计上的疏忽
一旦你意识到我们在 ViewState
中存在设计上的疏忽,修复实际上很简单。当用户面前呈现一个弹出窗口并且用户无法与新加载的数据交互时,为什么要更新导航并将数据加载到模块中?这没有任何意义。因此,修复方法是让 ViewState
在呈现弹出窗口时保留对应用程序其余部分的任何更新,并在弹出窗口关闭后触发事件。
要做到这一点的第一步是创建一个在 ViewState
中触发事件的通用方法。这就是我想出的
1: private void InvokeEvent<T>(MulticastDelegate target, T args) where T:EventArgs
2: {
3: if (target != null)
4: {
5: if (PopupBridge.Bridge.IsPopupOpen)
6: {
7: EventHandlerQueueItem<EventArgs> queueItem =
new EventHandlerQueueItem<EventArgs>();
8: queueItem.EventHandler = target;
9: queueItem.EventHandlerArguments = args;
10: _eventQueue.Enqueue(queueItem);
11: }
12: else
13: {
14: target.DynamicInvoke(new object[] { this, args });
15: }
16: }
17: }
InvokeEvent<T>
方法接受一个 MulticastDelegate
(它代表实际的事件实例) 和一个基于 T
的 args
参数,其中 T
应该始终是一个 EventArgs
或从它派生。为了使它尽可能容易使用,我首先检查是否存在有效的目标(如果没有可用的处理程序,则为 null
)。接下来,我检查是否有任何弹出窗口打开。在我们的例子中,有一个 PopupBridge
为我们提供该信息。现在,如果没有弹出窗口打开,我只需在目标上调用 DynamicInvoke
并传入对 ViewState
和 args 参数的引用。
但是,如果打开了一个 popup
,我将创建一个 EventHandlerQueueItem
。以下是该 struct
的外观
1: internal struct EventHandlerQueueItem<T> where T:EventArgs
2: {
3: public MulticastDelegate EventHandler;
4: public T EventHandlerArguments;
5: }
正如你所看到的,这是一个简单的 struct
,它既有一个 MulticastDelegate
,又可以包含要传递给任何处理程序的参数。一旦我们有了它,我们现在就可以声明一个 Queue<T>
来保存这些实例。由于我们无法编写像 Queue<EventHandlerQueueItem<T>>
这样的东西,我决定使用协变并使用 Queue<EventHandlerQueueItem<EventArgs>>
。
现在剩下的就是在弹出窗口关闭后触发事件。在我们的例子中,PopupBridge
知道这一点并为其触发一个事件。处理程序如下所示
1: void Bridge_PopupClosing(object sender, PopupClosingEventArgs e)
2: {
3: if (!PopupBridge.Bridge.IsPopupOpen)
4: {
5: while (_eventQueue.Count > 0)
6: {
7: EventHandlerQueueItem<EventArgs> item = _eventQueue.Dequeue();
8: if (item.EventHandler != null)
9: {
10: item.EventHandler.DynamicInvoke(new object[] { this,
item.EventHandlerArguments });
11: }
12: }
13: }
14: }
由于可以打开多个弹出窗口,我仍然需要检查那时是否打开了弹出窗口。如果它们全部关闭,我们可以继续调用 Dequeue
并调用处理程序,直到队列为空。
既然我们解决了这个设计上的疏忽,需要在任务中加载数据模块,将始终能够从服务器获取该数据,并且一切都按预期工作。
我希望你发现这些代码片段有用。如果您有任何问题或意见,请在下面留言。