从异步任务报告进度

5

大家好,

我遇到了一个小问题。我有一个WPF应用程序,它读取一个相当大的Excel文件,然后将其输出为XML文件。我面临的问题是,我想将操作的进度报告回处理程序。

我无法使其“平滑”运行,也就是说,GUI线程中的进度条一次性被填充,然后紧接着从Excel文件中读取的内容被填充到DataGrid中。

我对Async/Await/Task很陌生,以前一直在使用BackgroundWorker,但我希望更多地了解这方面的知识,所以如果这是一个愚蠢的问题,我很抱歉。

我已经阅读了Stephen Cleary的教程,链接在这里

老实说,我不知道为什么进度条不能像应该一样“平滑”填充...

在GUI中调用的代码:

        var progress = new Progress<int>(progressPercent => pBar.Value = progressPercent);

        Task.Run(() => _sourceService.ReadFileContent(filePath, progress)).ContinueWith(task =>
        {

            dataGrid.ItemsSource = task.Result.DefaultView;
            DataTable = task.Result;

            if (DataTable.Rows.Count > 0)
                BtnCreateXmlFile.IsEnabled = true;

        }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
ReadFileContent(filePath, progress)的方法体如下:
public DataTable ReadFileContent(string filePath, IProgress<int> progress)
    {
        var rowStart = 7;
        var columnStart = 1;

        var existingFile = new FileInfo(filePath);
        using (var package = new ExcelPackage(existingFile))
        {
            var worksheet = package.Workbook.Worksheets["BOP"];
            var dt = new DataTable();

            // Compose the name of the table from:
            // - Product Type
            // - Customer
            // - Customer Project
            // * These can be found in the "CollaborationContext" sheet that is present in the ExcelFile.
            if (package.Workbook.Worksheets["CollaborationContext"].SelectedRange["B6"].Value.Equals("object_type"))
            {
                dt.TableName = package.Workbook.Worksheets["CollaborationContext"].SelectedRange["C9"].Value + " " +
                               package.Workbook.Worksheets["CollaborationContext"].SelectedRange["D9"].Value + " " +
                               package.Workbook.Worksheets["CollaborationContext"].SelectedRange["E9"].Value;
            }


                dt.TableName = package.Workbook.Worksheets["CollaborationContext"].SelectedRange["B9"].Value + " " + 
                               package.Workbook.Worksheets["CollaborationContext"].SelectedRange["C9"].Value + " " +
                               package.Workbook.Worksheets["CollaborationContext"].SelectedRange["D9"].Value;


            // Report only in chunks. We do not want to call `progress.Report` for each row that is read.
            var totalRows = worksheet.Dimension.End.Row;
            var currentIndex = 0;
            var percentageProgress = totalRows / 10;


            // Get Columns and add them to the DataTable
            for (var col = columnStart; col <= worksheet.Dimension.End.Column - 1; col++)
                dt.Columns.Add(worksheet.Cells[6, col].Value.ToString());

            // Place data into DataTable
            for (var row = rowStart; row <= worksheet.Dimension.End.Row; row++)
            {
                var dr = dt.NewRow();
                var x = 0;
                currentIndex++;

                for (var col = columnStart; col <= worksheet.Dimension.End.Column - 1; col++)
                {
                    dr[x++] = worksheet.Cells[row, col].Value;
                }
                dt.Rows.Add(dr);

                // Report progress back to the handler
                if (currentIndex % percentageProgress == 0)
                    progress?.Report(row);
            }

            return dt;
        } 

    }

感谢您提前的帮助!

2
Excel文件中有多少行?进度条将在您达到第100行时显示100,因为这是您在progress?.Report()中报告的数字。或者类似于这样的东西。我真的不理解if (currentIndex % percentageProgress == 0),但也许我只需要更仔细地阅读。 - adv12
那取决于你。如果你不想这样做,你可以报告一个百分比而不是行索引。 - adv12
1
  1. 你应该在创建任务时使用await
  2. 当任务将会受到I/O限制时,应避免使用Task.Run()
  3. 你的进度pBar.Value = progressPercent委托没有在UI线程上执行,因此不会更新。
- user585968
@MickyD - 在 progress?.Report(row); 上设置了断点 - 它显示:未标记 > 2644 12 工作线程 TeamCenterExport.exe!TeamCenterExport.Service.ExcelSourceService.ReadFileContent 正常 - darksleep
尝试使用async标记ReadFileContent(string filePath, IProgress<int> progress)并返回一个Task<DataTable>,然后从GUI线程调用它,如var test = await _sourceService.ReadFileContent(filePath, progress),但是没有成功。 :( - darksleep
显示剩余7条评论
1个回答

8

首先,让我们摆脱危险的ContinueWith调用。你应该使用await代替:

var progress = new Progress<int>(progressPercent => pBar.Value = progressPercent);
var result = await Task.Run(() => _sourceService.ReadFileContent(filePath, progress));
dataGrid.ItemsSource = result.DefaultView;
DataTable = result;
if (DataTable.Rows.Count > 0)
  BtnCreateXmlFile.IsEnabled = true;

接下来,你可能会发现进度条的问题是,你的 Progress<int> 处理程序期望收到一个 progressPercent,但是 ReadFileContent 发送的是已读行数,而不是百分比。因此,为了解决这个问题:
if (currentIndex % percentageProgress == 0)
  progress?.Report(row * 100 / totalRows);

(这里还有其他几个选项; 例如,如果您想要更复杂的UI,则可以决定报告当前行和总行数)。

进度条未能“平滑”填充

上面所描述的是最低可接受的解决方案。特别是,“仅更新10次”的代码有点问题;它总是更新10次,而不管更新速度快慢如何。更一般的解决方案是使用基于Rx的IProgress解决方案,这将允许您根据时间进行节流(例如,每秒4次更新)。


我已经按照你的要求修改了代码,现在它可以正常工作了。我还修改了 progress?.Report() 方法,以便将百分比返回给 UI 线程中的进度条。但问题是,我真的需要阅读你关于 Async / Await / Task.Run 等方面的博客文章。你有什么起点可以让我阅读所有文章吗? - darksleep
@darksleep:不是所有的都需要,对于async/await,我建议从这里开始(http://blog.stephencleary.com/2012/02/async-and-await.html),对于`Task.Run`,我建议从这里开始(http://blog.stephencleary.com/2013/10/taskrun-etiquette-and-proper-usage.html)。 - Stephen Cleary
还有一个问题。我理解如果我调用Task.Run(()=>DoSomething()),那么线程池中的一个线程将被请求来执行任务。那么,如果我有一个类似上面的方法,并且我标记事件处理程序-Button1_Click()为async,在其内部调用await ReadContentFile(),没有Task.Run(),没有其他东西,这有什么区别呢? - darksleep
@darksleep:假设您还将ReadContentFile更改为异步方法,那么这将很好地工作。 - Stephen Cleary
那意味着我必须将ReadContentFile标记为async,并以某种方式使用await标记长时间运行的操作。在这种特殊情况下,由于整个方法都是读取Excel文件内容的for-loop,所以我没有什么可以用await标记的东西?换句话说,如果我在该方法上调用await,而该方法未标记为async,那么它基本上会同步运行? - darksleep
@darksleep:你应该从另一个方向来看待"async"。首先从最低级别的API开始,尽可能地使它们变成异步的。 - Stephen Cleary

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接