构建 Silverlight 企业应用程序时的冒险 - 第 38 部分
我们在构建 Silverlight 业务应用程序时遇到的大多数问题 - 它涉及到 UI 范式与异步通信模型的结合
在本文中,我想探讨在构建 Silverlight 业务线 (Line Of Business) 应用程序时,我们大多数人都会遇到的一个问题。它涉及到 UI 范式与异步通信模型的结合。
问题
我们都曾遇到过这种情况。你有一个需要用户填写的表单,其中包含几个字段和一个保存按钮。当用户点击保存按钮时,你会验证他们的输入,如果验证失败,你就会阻止保存操作。
一切都很顺利。用户可以根据需要更正输入并重试。然而,验证有一个缺点。它仅限于true
或false
,中间没有其他选项。
让我们考虑一个需要用户确认的场景。你可能会通过使用一个标志来 hack 这个验证系统,如果用户第二次保存时没有更改值,那么它肯定是正确的。然而,我认为这不是一个很好的解决方案,并且会让用户感到困惑。
我需要一个对话框!
所以我们需要一种其他方式来向用户请求输入。通常的做法是向用户展示一个对话框,用户可以通过按一个按钮来选择是否继续。我们与它们打了多年的交道,它们使用起来很直观。
然而,在 Silverlight 中,它们会带来问题。当然,你可以使用框架中包含的对话框。然而,这会使用浏览器来显示对话框,而且外观不佳。更重要的是,它的功能非常有限。幸运的是,构建一个提供我们所需功能的自定义用户控件并不难。我们甚至可以通过在整个 UI 上放置一个 Canvas
,然后将自定义用户控件放在 Popup
(或者使用 childwindow
) 中来实现模态效果。
然后问题就开始了。如果你要构建一个用户控件来显示对话框,就像我们所做的那样,你很可能通过回调来通知应用程序用户点击了对话框中的按钮,并且操作已完成。虽然这是一个经典的异步模型,但与之协作可能会变得困难。
拆分代码
让我们回到之前的场景。我们有一个包含一些字段的表单。有些字段需要验证,有些字段可能需要弹出一个对话框。在我们引入对话框之前,保存数据的步骤如下:
- 验证输入
- 如果所有字段都正确,则开始保存数据
- 保存完成后,刷新当前数据
现在,如果我们引入一个允许用户选择停止或继续的单个对话框,步骤将如下:
- 验证输入
- 如果所有字段都正确,则检查是否需要对话框
- 如果需要对话框,则显示对话框
- 一旦用户做出选择并希望继续,则开始保存数据。如果用户不想继续,则停止。
- 保存完成后,刷新当前数据
看看这些步骤,我们从一个用于保存完成的事件处理程序,变成了为对话框添加一个回调。所以,我们不再有两个包含保存代码的方法,而是有了三个方法。每增加一个对话框,我们就会增加一个回调,从而增加一个包含我们保存操作代码的方法。你可以看到这种情况很快就会变得难以管理。
所以让它变成同步的……
嗯,这可以解决我们的问题,但是…… Silverlight 只有一个 UI 线程的调度器。这意味着一旦你阻塞了 UI 线程,你的应用程序就会变得无响应(基本上,它会挂起直到你解除阻塞)。虽然显示一个对话框不是一件非常实际的事情,因为用户将无法点击按钮(这会破坏整个目的,对吧?)。
实际上,这意味着没有直接的方法可以提供同步的 UI 交互。它将是基于事件的,因此是异步的。不过,有一个解决办法。你不能阻塞 UI 线程,但你可以阻塞任何其他线程,所以如果我们把所有涉及保存的代码都移到一个后台线程,我们就可以消除对话框的回调。下面是一些示例代码,展示了调用这样的对话框会是什么样子:
1: private void saveButton_Click(object sender, RoutedEventArgs e)
2: {
3: // Make sure to create your dialog while still running on the UI thread
4: _dialog = new DialogWindow();
5:
6: // Pass off the save logic to another thread
7: ThreadPool.QueueUserWorkItem(SaveData, this);
8: }
9:
10: private void SaveData(object stateInfo)
11: {
12: if (DialogWindow.ShowDialog(_dialog, stateInfo))
13: {
14: // Actually save the data
15: }
16: }
所以基本上,你将保存操作中涉及的任何内容(除了验证,可能)放在一个后台线程上运行。我还传递了对调用 UI 控件的引用。原因很快就会清楚。
在后台线程上运行时,你可以调用我的 DialogWindow.ShowDialog
方法,它会阻塞你的线程。这意味着你可以立即使用它的结果。
在 UI 线程上运行代码
为了让这一切起作用,我们显然需要能够运行一些代码在 UI 线程上,然后阻塞我们的线程等待结果。下面是 ShowDialog
方法的代码:
1: public static bool ShowDialog(DialogWindow window, object stateInfo)
2: {
3: bool result = false;
4: DependencyObject dependencyObject = stateInfo as DependencyObject;
5:
6: // If we got passed a dependency object and we have access
7: // to its dispatcher, then we are on the UI thread and we can't block
8: if (dependencyObject == null dependencyObject.Dispatcher.CheckAccess())
9: {
10: window.Show();
11: return false;
12: }
13:
14: Action action = new Action(window.Show);
15: // Run the Show method on the UI thread
16: dependencyObject.Dispatcher.BeginInvoke(action);
17: // Block this thread until we have a result
18: while (!window.DialogResult.HasValue)
19: {
20: Thread.Sleep(50);
21: }
22: result = window.DialogResult.Value;
23:
24: return result;
25: }
我们首先要做的是检查我们是否真的在 UI 线程上。如果我们是在 UI 线程上,我们就不想阻塞。请注意,CheckAccess
方法不知何故没有显示在 IntelliSense 中。尽管如此,它编译起来没有问题。
如果我们在 UI 线程上,我们就可以使用传递给我们的依赖对象的调度器,在 UI 线程上实际运行 Show
方法。然后,我们只需等待结果并将其传回。
结论
为了实现同步对话框,我们只是引入了一个线程来运行我们的代码,该线程在 UI 线程上运行一些代码,并等待其完成。这应该使我们更容易地在代码中组合更复杂的流程。
你可以在 此处 下载代码。