在开发Silverlight应用程序时,经常会遇到需要用户输入的场景。例如,一个表单需要用户填写,包含几个字段和一个保存按钮。用户点击保存按钮时,会验证输入,如果验证失败,则阻止保存操作。这种验证机制虽然有效,但它的反馈只有“通过”或“不通过”,没有中间状态。
设想一个场景,需要用户的确认。虽然可以通过在验证系统中使用一个标志来实现,如果用户没有改变值并且第二次保存,那么它就是正确的。但这不是一个很好的解决方案,它可能会让用户感到困惑。
因此,希望有其他方式来请求用户输入。通常的做法是向用户展示一个对话框,用户可以通过点击按钮来选择是否继续。已经使用它们很多年了,它们直观易用。
然而,在Silverlight中,对话框存在问题。当然,可以使用框架中包含的对话框。但是,这会使用浏览器来展示对话框,这看起来并不美观。更重要的是,它的功能非常有限。幸运的是,构建一个提供想要功能的自定义用户控件并不是什么大问题。甚至可以通过在UI上放置一个Canvas
,然后在上面放置一个Popup
(或者使用ChildWindow
),来使其成为模态的。
让回到场景。有一个包含一些字段的表单。一些字段需要验证,一些字段可能会触发对话框。在引入对话框之前,保存数据的步骤如下:
现在,如果引入一个允许用户选择停止或继续的对话框,步骤如下:
从这些步骤中,可以看到,从只有一个保存完成的事件处理程序,到添加一个对话框的回调。所以,不是有两个保存代码的方法,而是有三个方法。并且随着添加的每个对话框,又增加了一个回调,从而又增加了一个包含保存操作代码的方法。可以看到这很快就会变得难以控制。
那么,这将解决问题,然而……Silverlight只有一个UI线程的调度器。这意味着,一旦阻塞了UI线程,应用程序就会变得无响应(基本上,它会挂起,直到解除阻塞)。在显示对话框时这样做并不实用,因为用户将无法点击按钮(这完全违背了目的,对吧?)。
实际上,这意味着没有直接的方式来提供同步的UI交互。它将是事件驱动的,因此是异步的。然而,有一种方法可以解决这个问题。不能阻塞UI线程,但可以阻塞任何其他线程,所以如果将所有涉及保存的代码移动到后台线程,可以消除对话框的回调。以下是调用这样一个对话框的示例代码:
private void saveButton_Click(object sender, RoutedEventArgs e)
{
// 确保在UI线程上创建对话框
_dialog = new DialogWindow();
// 将保存逻辑传递给另一个线程
ThreadPool.QueueUserWorkItem(SaveData, this);
}
private void SaveData(object stateInfo)
{
if (DialogWindow.ShowDialog(_dialog, stateInfo))
{
// 实际保存数据
}
}
基本上,将与保存操作相关的任何代码(除了验证)在后台线程上运行。还传递了一个对调用UI控件的引用。原因很快就会变得清晰。
为了使这工作,显然需要能够在UI线程上运行一些代码,然后阻塞线程以等待结果。以下是ShowDialog
方法的代码:
public static bool ShowDialog(DialogWindow window, object stateInfo)
{
bool result = false;
DependencyObject dependencyObject = stateInfo as DependencyObject;
// 如果得到了一个依赖对象,并且可以访问它的调度器,
// 那么就处于UI线程,不能阻塞
if (dependencyObject == null || dependencyObject.Dispatcher.CheckAccess())
{
window.Show();
return false;
}
Action action = new Action(window.Show);
// 在UI线程上运行Show方法
dependencyObject.Dispatcher.BeginInvoke(action);
// 阻塞此线程,直到有结果
while (!window.DialogResult.HasValue)
{
Thread.Sleep(50);
}
result = window.DialogResult.Value;
return result;
}
首先检查否真的在UI线程上。如果在,不想阻塞。请注意,CheckAccess
方法不会在IntelliSense中显示,但它可以毫无问题地编译。
如果不在UI线程上,实际上可以使用传递给依赖对象的调度器,来在UI线程上运行Show
方法。然后,简单地等待结果并将其传回。