Question

I've got the following code, As you can see the background worker searches for files and in the progress changed event files are added to a listview, however since there is lots of files being added to the listview, the UI becomes unresponsive, I could Sleep the thread in the loops but I don't think that's a good practice, what's the best way to prevent the UI freeze?

to elaborate more, listview is a form control on a form.

void bg_DoWork(object sender, DoWorkEventArgs e)
{
    Stack<string> dirs = new Stack<string>(20);
    dirs.Push(e.Argument.ToString());
    while (dirs.Count > 0)
    {
        string currentDir = dirs.Pop();
        string[] subDirs;
        try { subDirs = System.IO.Directory.GetDirectories(currentDir); }
        catch (UnauthorizedAccessException) { continue; }
        catch (System.IO.DirectoryNotFoundException) { continue; }

        string[] files = null;
        try { files = System.IO.Directory.GetFiles(currentDir); }

        catch (UnauthorizedAccessException) { continue; }
        catch (System.IO.DirectoryNotFoundException) { continue; }
        foreach (var file in files) { bg.ReportProgress(0, file); }
        foreach (string str in subDirs) { dirs.Push(str); }
    }
}
    void bg_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        listView1.Items.Add(e.UserState.ToString());
    }
Was it helpful?

Solution

So the issue here is that ReportProgress is actually asynchronous. It doesn't wait for the corresponding UI updates to actually be made before it continue moving on doing work. Normally this is great. In most situations there's no compelling reason to slow down your productive work just to go wait for UI updates.

There's one exception though. If you call ReportProgress so often that it doesn't actually have time to complete the previous progress update before the next one is added, what ends up happening is that you fill up the message queue with requests to go update progress. You have so many files, and getting those lists of files takes very little time. It actually takes quite a bit less time than it takes to marshal to the UI thread and update the UI.

Because this queue ends up being backed up, any other UI updates need to sit through that long queue before they can do anything.

Batching up the updates and indicating progress less often is one possible solution. It may or may not be acceptable, given your situation. It will almost certainly help, but depending on just how long it takes the UI to be updated based on whatever it is that you're doing, and how quickly you can generate data, it's possible that even this will cause problems. If it works for your specific case though, great.

The other option is to change how you update progress such that your worker waits for the UI update before continuing. Obviously this is something that you should avoid unless you need to do it, because it means that while you aren't freezing the UI while you do your work, your work will take a fair bit longer. While there are any number of ways of doing this, the simplest of which is likely to just use Invoke (not BeginInvoke):

foreach (var file in files)
    listView1.Invoke(new Action(()=>listView1.Items.Add(file));

While calling Invoke from a BackgroundWorker is generally code smell, and should be avoided, this is a somewhat exceptional case.

Note that even if you do end up resorting to using Invoke here I would still suggest batching the invokes such that you add more than just one item per invoke. If the number of files in a single directory is sufficiently low, put the whole foreach inside the Invoke, and if your subdirectories tend to have very few files (i.e, they're very deep, not broad) consider even putting all of the files into a temp list until it's large enough to be worth batching into an Invoke. Play around with different approaches based on your data, to see what works best.

OTHER TIPS

bg.ReportProgress() is meant to report the overall progress of the BackgroundWorker back to the UI thread, so you can inform your users as to the progress. However, you're using it to actually add strings to a ListView. You're better off compiling the list of files into an in-memory list and then populating listView1 once when the background worker completes:

public void bg_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
   foreach (var file in MyFileListVar){
       listView1.Items.Add(file);
   }
}

Try to load multiple files (lets say between 10~50), then send them back to the UI thread (ie: bg.ReportProgress) instead of sending every file separately.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top