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

gr8qqesn  于 6个月前  发布在  其他
关注(0)|答案(2)|浏览(69)

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

public partial class CancellableProgressForm : Form
    {
        public CancellableProgressForm()
        {
            InitializeComponent();
        }

        public int PercentComplete
        {
            set
            {
                progressBar.Value = value;
            }
        }

        private void buttonCancel_Click(object sender, EventArgs e)
        {
            DialogResult = DialogResult.Cancel;
            this.Close();
        }
    }

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

private void buttonRun_Click(object sender, EventArgs e)
        {
            var progressForm = new CancellableProgressForm();

            var progress = new Progress<int>(percent =>
            {
                progressForm.PercentComplete = percent;
            });

            Task.Run(() =>
            {
                DoWork(progress);
                progressForm.DialogResult = DialogResult.OK;
                progressForm.Close();
            });
            DialogResult dr = progressForm.ShowDialog();
            MessageBox.Show($"Result={dr}");
        }

        // Prototype code to "do some work" and update the progress bar on the progress form
        private void DoWork(IProgress<int> progress)
        {
            for (int i = 1; i <= 100; ++i)
            {
                Thread.Sleep(100);
                progress.Report(i);
            }
        }


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

6yoyoihd

6yoyoihd1#

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

private async void buttonRun_Click(object sender, EventArgs e)
{
    CancellableProgressForm progressForm = new();
    progressForm.Show();

    IProgress<int> progress = new Progress<int>(percent =>
    {
        progressForm.PercentComplete = percent;
    });

    await Task.Run(() => DoWork(progress));

    MessageBox.Show("Done");
}

字符串
上面的代码只是朝着正确的方向迈出了一步,而不是一个完整的解决方案。
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()返回时完成。

public partial class CancellableProgressForm : Form {
    CancellationTokenSource cts = new CancellationTokenSource();
    public CancellableProgressForm() => InitializeComponent();

    private void buttonCancel_Click(object sender, EventArgs e) => cts.Cancel();

    protected override async void OnLoad(EventArgs e) {
        base.OnLoad(e);

        bool errorState = false;
        var progress = new Progress<int>(percent => progressBar.Value = percent);
        try {
            await Task.Run(() => DoWork(progress, cts.Token));
            DialogResult = DialogResult.OK;
        }
        catch (OperationCanceledException) {
            DialogResult = DialogResult.Cancel;
        }
        catch (Exception) {
            errorState = true;
            throw;
        }
        finally {
            cts?.Dispose();
            if (errorState) DialogResult = DialogResult.Abort;
        }
    }
}

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

private void DoWork(IProgress<int> progress, CancellationToken token = default) {
    int progressCount = 1;
    while (progressCount <= 100) {
        token.ThrowIfCancellationRequested();
        Thread.Sleep(100);
        progress.Report(progressCount);
        progressCount += 1;
    }
}


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

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


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

相关问题