伪弹窗实现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_ClickCalculateCount 的签名均被修改掉,这还是调用层次不深的情况,如果,调用层次太深,每个调用方法都需要需改方法签名,如果这些方法引用较多的话,会引起大量的修改,可能还会存在一些方法签名不能被修改的情况。

  • 在遇到这个问题时,我思索过很多方案,突然想起来一个场景,那就是弹窗 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()也会卡死界面
posted on 2025-04-17 14:22  baby-jie  阅读(25)  评论(0)    收藏  举报