Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reporting progress from Async Task

Hello fellow developers,

I am facing a slight dilemma. I have a WPF application that reads an rather large Excel File and after that it outputs it as an XML file. The problem that I am facing is that I want to report the progress of the operation back to the handler.

I cannot get it to work "smoothly", literally the Progress Bar that is in the GUI Thread gets filled at once and immediately after that the DataGrid that holds the content read from the Excel File gets filled.

I am new to Async/Await/Task, I've been using BackgroundWorker until now but I wish to gain more knowledge about this, so I'm sorry if this is a dumb question.

I have read the tutorial from Stephen Cleary from here.

I honestly have no idea why the Progress Bar does not fill "smoothly" as it should be...

Code that is getting called in the 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());

The method body of 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;
        } 

    }

Thank you in advance !

like image 614
darksleep Avatar asked Mar 10 '23 00:03

darksleep


1 Answers

First, let's get rid of the dangerous ContinueWith call. You really should use await instead:

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;

Next, the problem you're likely seeing with progress is that your Progress<int> handler is expecting a progressPercent, but ReadFileContent is sending how many rows it's read, not a percentage. So, to fix that:

if (currentIndex % percentageProgress == 0)
  progress?.Report(row * 100 / totalRows);

(there are several other options here; e.g., you could decide to report the current row and the total rows if you want a fancier UI).

the Progress Bar does not fill "smoothly" as it should be

What I've described above is the minimum acceptable solution. In particular, the "only update 10 times" code is a bit problematic; it always updates 10 times, regardless of how fast or slow the updates are. A more general solution would be to use an Rx-based IProgress solution, which would allow you to throttle based on time (e.g., 4 updates per second).

like image 127
Stephen Cleary Avatar answered Mar 20 '23 15:03

Stephen Cleary