WPF多线程的一种解决方案
最近使用WPF来构建桌面程序,其中要求是:
1. 界面与后台程序使用不同线程,以便在后台运行耗时计算时界面依然响应。
2. 后台程序需要控制界面某些元素,如显示和修改值。
3. 后台程序执行中,需要获取界面把某些用户输入,如用户输出某字符串来继续执行。
然后开始设计,编码测试,过程如下:
对于第1个要求,根据以前的编程经验(Qt,GTK...),果断使用System.Threading.Thread来Start一个线程,与WPF界面线程分开,嗯。
对于第2个要求,使用DataBinding,把WPF界面的类的DataContext设置成给线程用的类里,绑定值,就很容易做到在第二个线程修改值直接反应到WPF的界面上。但对于切换界面(如修改MainWindow的Content给另外一个UserControl),搞得非常麻烦。想过通过绑定Visibility属性,添加IsVisibilityChange方法来做,效果都非常差。从MSDN看WPF多线程的例子http://msdn.microsoft.com/en-us/library/ms741870.aspx,想着把第1个要求使用System.Threading.Thread改成Dispatcher.BeginInvoke来运行后台线程,这样就可以随便更改WPF的元素了。
接着第3个要求,考虑到.Net有ManualResetEvent(或者AutoResetEvent)这个很方便的类,果断写下这个UserInput类(暂时使用string来作为用户输入的表现)。
然后在后台线程调用WaitInput,这下麻烦出来了,WPF界面线程也一起Block掉了。
于是不停的修改代码,尝试网上搜索到的方法,均不能解决。然后回头再仔细看MSDN关于WPF多线程的例子,每一个字都认真的读,发现Dispatcher是唯一的多线程可以修改WPF元素的类。那不知道我在另外一个线程来调用Dispatcher.BeginInvoke来修改WPF元素可否?立即编写代码尝试。结果是可行的。最终程序结构如下:
整个程序只有一个Window类,那就是从Window派生出来的MainWindow。定义若干UserControl与之相关的Presenter类(用于数据绑定和界面切换),并在MainWindow构造函数里一一生成并绑定,所有的Presenter有Dispatcher属性,所有的Presenter有若干delegate用于数据绑定不能实现的操作(定义一个基类即可),如更改MainWindow的Content,并在各个UserControl里定义相应的函数,赋于这些delegate,在Presenter调用这些delegate的时候,使用Dispatcher.BeginInvoke来调用。初始Content为某一UserControl。使用BackgroundWorker来启动后台线程,定义一个带ManualResetEvent的UserInput来给线程得到WPF输入。
这个后台线程就可以随时得到用户的输入,如用户想切换到某个特定界面, 后台线程得到这个输入后可以很方便的切换界面。
一些参考的类:
2 using System.Threading;
3
4 namespace WPF
5 {
6 public class UserInput
7 {
8 // Won't send signal when construct.
9 private ManualResetEvent _mre = new ManualResetEvent(false);
10 private string _currentInput = null;
11
12 /// <summary>
13 /// Current Input, if not input, return null or "", using string.IsNullOrEmpty to determine.
14 /// </summary>
15 public string CurrentInput
16 {
17 get
18 {
19 string result = _currentInput;
20 _currentInput = null;
21 return result;
22 }
23 set
24 {
25 _currentInput = value;
26 _mre.Set();
27 }
28 }
29
30 public string WaitInput
31 {
32 get
33 {
34 _mre.WaitOne();
35 _mre.Reset();
36 return CurrentInput;
37 }
38 }
39
40 public const string BackString = "..";
41 }
42 }
2 using System.ComponentModel;
3
4 namespace WPF
5 {
6 public abstract class Notifier : INotifyPropertyChanged
7 {
8 public event PropertyChangedEventHandler PropertyChanged;
9
10 protected virtual void OnProertyChanged(string propertyName)
11 {
12 if (PropertyChanged != null)
13 {
14 PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
15 }
16 }
17 }
18 }
2 using System.Windows;
3 using System.Windows.Threading;
4
5 namespace WPF
6 {
7 public class PresenterBase : Notifier
8 {
9 private string _title;
10 private Dispatcher _dispatcher = null;
11 public delegate void NoArgDelegate();
12 public NoArgDelegate ShowHandler;
13
14 public PresenterBase()
15 {
16
17 }
18
19 public PresenterBase(string title)
20 {
21 _title = title;
22 }
23
24 public Dispatcher Dispatcher
25 {
26 get { return _dispatcher; }
27 set
28 {
29 _dispatcher = value;
30 }
31 }
32
33 public string Title
34 {
35 get { return _title; }
36 set
37 {
38 _title = value;
39 OnProertyChanged("Title");
40 }
41 }
42
43 public void Show()
44 {
45 if (ShowHandler != null)
46 Dispatcher.BeginInvoke(ShowHandler);
47 }
48 }
49 }