伪弹窗实现UI线程等待耗时操作完成
- 在日常开发中,经常遇到主线程因为某些耗时操作导致UI卡死的情况,一般的解决方案是开启另外一个线程去做耗时操作,等耗时操作完成再调用 Dispatch 在主线程中执行结果(比如界面显示等)。
举个栗子
-
现有一个
StudentService
类,可以计算学生的数量public int CalculateCount() { Thread.Sleep(10000); // 模拟耗时操作 return 100; }
-
现在需要做一个窗口程序,在点击按钮后,弹窗显示学生的10倍数量(学校为了招生,夸大事实)
public class MainWindow: Window { public StudentService StudentService { get; set; } private int CalculateCount() { return StudentService.CalculateCount() * 10; } private void CalculateBtn_Click(object sender, RoutedEventArgs e) { int studentCnt = CalculateCount(); MessageBox.Show(studentCnt.ToString()); } }
-
上述代码在业务上没有问题,但是在运行的时候,点击按钮后会卡住界面10秒钟,这对于窗体程序来说是不可接受的,所以在实现之初,一般都会采用
async/await
private async void CalculateBtn_Click(object sender, RoutedEventArgs e) { int studentCnt = await CalculateCount(); MessageBox.Show(studentCnt.ToString()); } private Task<int> CalculateCount() { return Task.Run(() => { return StudentService.CalculateCount() * 10; }); }
-
在设计之初,这么写一点问题没有,但是对于程序已经写好,才发现这个情况,再去改为这种方式需要一些代价,因为
async/await
具有污染性,对比上述两份代码会发现CalculateBtn_Click
和CalculateCount
的签名均被修改掉,这还是调用层次不深的情况,如果,调用层次太深,每个调用方法都需要需改方法签名,如果这些方法引用较多的话,会引起大量的修改,可能还会存在一些方法签名不能被修改的情况。 -
在遇到这个问题时,我思索过很多方案,突然想起来一个场景,那就是弹窗
win.ShowDialog()
既让调用的主线程在等待弹窗的结果,又没有阻塞主线程,于是探索了弹窗的机制
FakeWindow 实现主线程等待机制
-
其实wpf的窗口的
ShowDialog
的底层是靠DispatchFrame
来实现的,以下是实现伪弹窗的简洁版本public class FakeWindow { private DispatcherFrame _frame; public void ShowDialog() { _frame = new DispatcherFrame(); Dispatcher.PushFrame( _frame ); } public void Close() { if (_frame != null) { _frame.Continue = false; _frame = null; } } }
-
接下来在上述例子中应用这个伪弹窗
private void CalculateBtn_Click(object sender, RoutedEventArgs e) { FakeWindow window = new FakeWindow(); var task = Task.Run(() => { int ret = CalculateCount(); window.Close(); return ret; }); window.ShowDialog(); MessageBox.Show(task.Result.ToString()); } private int CalculateCount() { return StudentService.CalculateCount() * 10; }
-
最后来检验下效果:没有修改函数签名,没有卡死主线程,正常弹出结果弹窗。
-
notes:
- 在主线程中调用
SemaphoreSlim.Wait()
也会卡死界面
- 在主线程中调用