Progress Bar for reading a file - unexpected UI behavior

1k views Asked by At

I am trying to update a progress bar while reading a file. The file size will vary between 200Kb up to 50Mb.

I am using System.ComponentModel.BackgroundWorker for the reading process, with these definitions:

progressBar.Minimum = 0

progressBar.Maximum = System.IO.FileInfo.Length (I don't care about percentages).

The reading process:

void worker_DoWork(object sender, DoWorkEventArgs e)
    {
        BackgroundWorker bg = sender as BackgroundWorker;

        while (!reader.EndOfStream)
                {
                    line = reader.ReadLine();
                    file_content.Add(line);
                    progress_precentage += line.Length + 2;
                    System.Threading.Thread.Sleep(100);
                    bg.ReportProgress(progress_precentage);
                }
    }

And the update process:

void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        progressBar.Value = e.ProgressPercentage;

        labelProgress.Content = "reading " + e.ProgressPercentage + " out of " + file_length + " bytes";
    }

The UI's reaction is very strange. For a 300Kb file, the progress bar and label are not even updated. They reach the maximum value immediately. For a 50Mb file, they are updated 4 times before completing in under a second.

So I added System.Threading.Thread.Sleep:

while (!reader.EndOfStream)
                {
                    line = reader.ReadLine();
                    file_content.Add(line);
                    progress_precentage += line.Length + 2;
                    System.Threading.Thread.Sleep(100);
                    bg.ReportProgress(progress_precentage);
                }

This caused the 300Kb file to take roughly a minute to complete, and the 50Mb file.. you get the point.

When I used System.Threading.Thread.Sleep(1), the 300Kb file got to about half way very fast and actually SLOWED DOWN until completing in roughly 5 seconds. The 50Mb file took a very long time to complete.

Of course I can fiddle around with the Thread.Sleep so it will fire once every 10 lines or so, but the performance will change depending on the file size.

Is there a way to consider the file size so that the process will finish in 2~3 seconds regardless of the file size? I know it's possible since reading a 50Mb file takes less than a second to complete (without Thread.Sleep).

Thanks!

EDIT: CODE AFTER SUGGESTIONS (Couldn't submit it as answer for some reason):

void worker_DoWork(object sender, DoWorkEventArgs e)
    {
        BackgroundWorker bg = sender as BackgroundWorker;

        try
        {
            file_content = System.IO.File.ReadAllLines(file_path).ToList();
        }
        catch ()
        {
            bg.ReportProgress(-1);
            file_read_successful = false;
            return;
        }

        //For i from 0 to 100
        System.Threading.Thread.Sleep(10);
        bg.ReportProgress(i);

        file_read_successful = true;
    }

void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)        {
        //Failure to read file
        if (e.ProgressPercentage < 0)
        {
            //Show popup with failure message
            textBlockFailure.Text = (string)e.UserState;
            popupSelect.IsOpen = true;
            return;
        }

        labelProgress.Content = e.ProgressPercentage + "%";
        progressBar.Value = e.ProgressPercentage;
    }

void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if (file_read_successful)
        {
            labelProgress.Content = "Done!";
            progressBar.Value = progressBar.Maximum;
        }
        else
        {
            labelProgress.Content = "";
            progressBar.Value = progressBar.Minimum;
        }
        //Unregister events
        worker.DoWork -= worker_DoWork;
        worker.ProgressChanged -= worker_ProgressChanged;
        worker.RunWorkerCompleted -= worker_RunWorkerCompleted;
    }
3

There are 3 answers

1
Aaron Thomas On BEST ANSWER

Since you are trying to make the progressbar take a minimum of 3 seconds, regardless of file size, bandwidth, other processes on host or client computer, etc... well, there are really only two choices.

The first choice is to make the progressbar continue even after the download is complete. There are several options with this (show 100% completed for the remaining time, manipulate so it returns an untrue value after downloading, etc).

The second choice is to throttle the actual download, as you have already practiced. Again, there are many factors present here that are outside control of your code. So, I would suggest adding some calculations so you know how to throttle.

To remark more on the second choice: you've already shown a basic way to do this by making throttling a percentage of the time it takes to download. You can build on this by reading file size beforehand, and calculating from that. Another option would be to do a partial download of the file (1000 lines, for example), see how long that takes, and extrapolate out for a guess of how long it will take to download the entire file.

As a case in point of how difficult this can be - if you've seen a MS operating system copying files and displaying "time remaining," how often has that been correct or consistent, even during file transfer?

Of course you're not calculating time remaining but showing a progress bar. But I would maintain that you're running into the same fundamental hurdles.

0
Derorrist On

Thank you all for the responses.

After a good night's sleep and with Tony Hinkle's response in mind, I decided throttling the file read is indeed a bad idea. So what I did is read the file, and then update the progress bar with Thread.Sleep(10), taking approx. 2 seconds to complete.

The user will only see a slight delay for 50Mb files, and none whatsoever for smaller files.

It's a bit cheating, but overall a fast and UI friendly solution.

Aaron Thomas's first choice in his response provided the overall idea for implementation, so it's the accepted answer.

Thanks again for your suggestions!

2
Sheridan On

After reading your question, the first thing that I noticed was that the FileInfo.Length property is of type long and the ProgressBar.Maximum property is of type double, so you could easily have problems there.

The next thing that I noticed is that you are calling Thread.Sleep(100); which is a terrible idea. The Thread.Sleep method will block the UI thread and so is not a good way to pause execution. Instead of that, you should try using the Task.Delay method.

Next, I noticed that your call to progress_precentage += line.Length + 2 will result in a total progress_precentage that does not match the Maximum value that you set earlier. If you resolve these issues, it may help.