winforms 可取消的进度表单和跨线程操作

gr8qqesn  于 2023-11-21  发布在  其他
关注(0)|答案(2)|浏览(231)

我想创建一个有进度条和取消按钮的表单。我的程序中的代码将运行一个算法并更新进度条。我实现了一个原型(下面)。我想知道这是否是一个正确的实现,或者是否有更好的方法来做到这一点。特别是,我想知道跨线程访问UI进度条是否有任何问题。我过去见过代码可以(看起来)正确运行,但在调试器下运行时会引发跨线程异常。
下面是实现可取消进度条的基本表单。

  1. public partial class CancellableProgressForm : Form
  2. {
  3. public CancellableProgressForm()
  4. {
  5. InitializeComponent();
  6. }
  7. public int PercentComplete
  8. {
  9. set
  10. {
  11. progressBar.Value = value;
  12. }
  13. }
  14. private void buttonCancel_Click(object sender, EventArgs e)
  15. {
  16. DialogResult = DialogResult.Cancel;
  17. this.Close();
  18. }
  19. }

字符串
以及在带有运行按钮的表单上测试它的原型代码:

  1. private void buttonRun_Click(object sender, EventArgs e)
  2. {
  3. var progressForm = new CancellableProgressForm();
  4. var progress = new Progress<int>(percent =>
  5. {
  6. progressForm.PercentComplete = percent;
  7. });
  8. Task.Run(() =>
  9. {
  10. DoWork(progress);
  11. progressForm.DialogResult = DialogResult.OK;
  12. progressForm.Close();
  13. });
  14. DialogResult dr = progressForm.ShowDialog();
  15. MessageBox.Show($"Result={dr}");
  16. }
  17. // Prototype code to "do some work" and update the progress bar on the progress form
  18. private void DoWork(IProgress<int> progress)
  19. {
  20. for (int i = 1; i <= 100; ++i)
  21. {
  22. Thread.Sleep(100);
  23. progress.Report(i);
  24. }
  25. }


就像我说的,不管有没有调试器,代码都运行得很好。那么为什么不抛出跨线程异常呢?有没有更好的方法来做到这一点呢?

6yoyoihd

6yoyoihd1#

规则是你只能从创建它的线程与UI元素交互。UI组件是线程仿射的。它们不仅不是线程安全的,甚至不是自由线程的。所以要非常小心你在Task.Run委托中所做的事情。使用Progress<T>是可以的,因为这个组件在创建的时候捕获了同步上下文,并通过捕获的同步上下文传播所有报告,从而有效地实施上述线程亲和性规则。
你需要做的是将事件处理程序转换为async void,然后将await转换为Task.Run。在转换await之后,你就回到了UI线程上。

  1. private async void buttonRun_Click(object sender, EventArgs e)
  2. {
  3. CancellableProgressForm progressForm = new();
  4. progressForm.Show();
  5. IProgress<int> progress = new Progress<int>(percent =>
  6. {
  7. progressForm.PercentComplete = percent;
  8. });
  9. await Task.Run(() => DoWork(progress));
  10. MessageBox.Show("Done");
  11. }

字符串
上面的代码只是朝着正确的方向迈出了一步,而不是一个完整的解决方案。
Progress<T>类异步地报告进度,这是高效的,因为它不会减慢后台工作,但它可能会产生一些奇怪的排序工件。如果你遇到问题,你可以尝试我在这里发布的同步IProgress<T>实现。
至于如何实现取消功能,您应该通过添加CancellationToken参数来使DoWork可取消。详细信息请参阅本文:Cancellation in Managed Threads
另一篇可能更有用的文章:Async in 4.5: Enabling Progress and Cancellation in Async APIs

展开查看全部
baubqpgj

baubqpgj2#

你不能运行一个任务,应该更新一个表单的方式,你现在这样做。
即使希望在此期间不会出现任何问题,您也希望显示进度的Form准备好处理IProgress委托生成的更新。
你需要处理例外情况。
顺便说一句,我已经测试了你的代码,正如预期的那样,当你试图访问Form类,设置它的DialogResult时,它确实会抛出。
您拥有的属性是通过Progress方法访问的。
我的建议是:从CancellableProgressForm运行DoWork()方法。
现在你完全控制了那里发生的一切
我添加了一个CancellationTokenSource,可以取消进度。
DoWork()方法现在接受一个CancellationToken,当按下取消按钮时,它报告一个取消请求。在本例中,对话框结果被设置为DialogResult.Abort
顺便说一句,当你设置一个窗体的DialogResult时,这也会关闭窗体。
因为它是一个模态对话框,所以你需要处理它。这在ShowDialog()返回时完成。

  1. public partial class CancellableProgressForm : Form {
  2. CancellationTokenSource cts = new CancellationTokenSource();
  3. public CancellableProgressForm() => InitializeComponent();
  4. private void buttonCancel_Click(object sender, EventArgs e) => cts.Cancel();
  5. protected override async void OnLoad(EventArgs e) {
  6. base.OnLoad(e);
  7. bool errorState = false;
  8. var progress = new Progress<int>(percent => progressBar.Value = percent);
  9. try {
  10. await Task.Run(() => DoWork(progress, cts.Token));
  11. DialogResult = DialogResult.OK;
  12. }
  13. catch (OperationCanceledException) {
  14. DialogResult = DialogResult.Cancel;
  15. }
  16. catch (Exception) {
  17. errorState = true;
  18. throw;
  19. }
  20. finally {
  21. cts?.Dispose();
  22. if (errorState) DialogResult = DialogResult.Abort;
  23. }
  24. }
  25. }

字符串
DoWork()方法可以解耦。它可以是Form类的成员,也可以在其他地方定义,它不与任何Form共享任何东西。

  1. private void DoWork(IProgress<int> progress, CancellationToken token = default) {
  2. int progressCount = 1;
  3. while (progressCount <= 100) {
  4. token.ThrowIfCancellationRequested();
  5. Thread.Sleep(100);
  6. progress.Report(progressCount);
  7. progressCount += 1;
  8. }
  9. }


在调用表单中,您只需显示对话框:

  1. private void buttonRun_Click(object sender, EventArgs e)
  2. {
  3. using (var progressForm = new CancellableProgressForm()) {
  4. DialogResult result = progressForm.ShowDialog();
  5. MessageBox.Show($"Result={result}");
  6. }
  7. }


剩下的:如果用户使用[X]按钮关闭表单(假设它是可见的).你可以用一行或两行代码解决它。三行代码仍然是可以接受的:)只是开玩笑,但你必须处理它。
目前,如果在任务运行时关闭窗体,则会得到一个DialogResult.Cancel。它可能不是您想要的 * 信号 *,因为用户没有明确按下分配给取消操作结果的按钮

展开查看全部

相关问题