Multiple threads tasks updating 1 progressbar - UI C# WPF

Viewed 598

I've been looking all around to find someone with a similar issue, but did not find anything. I'm coding a C# application using WPF UI.

To update the progressbar, I cannot use multithreading like I used to (in CLI) because it's telling me that I cannot update UI elements if it does not come from the main thread.

One solution is to create background workers. I've implemented this solution and it works well, but I want the tasks to be divided between more workers/threads (multithreading) in order to be more efficient.

I do not know the direction I have to take. If anyone can orient me with this issue, it would be more welcome.

Here is my code : (used to code with a MVVM pattern, just here to paste my code it's simpler for you)

public partial class testFunctionTaskPanel: Page
{
    private BackgroundWorker backgroundWorker1 = new BackgroundWorker();

    private string myURL;

    public testFunctionTaskPanel()
    {
        InitializeComponent();
    }

    private void startButton_Click(object sender, RoutedEventArgs e)
    {
        myURL = myURL.Text;

        myResults.Items.Clear();
        myResults.Items.Add("----Starting----");

        backgroundWorker1.WorkerReportsProgress = true;
        backgroundWorker1.ProgressChanged += ProgressChanged;
        backgroundWorker1.DoWork += DoWork;
        backgroundWorker1.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted;
        backgroundWorker1.RunWorkerAsync();
    }

    private void DoWork(object sender, DoWorkEventArgs e)
    {
        int length = myLoadedList.Items.Count;


        for (int i = 1; i <= length; i++)
        {
            try
            {
                HttpRequest req = new HttpRequest();
                req.Proxy = null;
                req.ConnectTimeout = 5000;
                req.IgnoreProtocolErrors = true;

                string get = myURL + myLoadedList.Items[i].ToString();
                var response = req.Get(get);

                if (response.StatusCode == Leaf.xNet.HttpStatusCode.OK)
                {
                    this.Dispatcher.Invoke(() =>
                    {
                        myResults.Items.Add(myLoadedList.Items[i].ToString());
                    });
                }
            }
            catch{}
            backgroundWorker1.ReportProgress(i);
        }
    }

    private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        myResults.Items.Add("----Finish----");
    }

    private void ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        // This is called on the UI thread when ReportProgress method is called
        progressbar.Value = e.ProgressPercentage;
    }
}
3 Answers

You can use Dispatcher.Invoke():

Dispatcher.Invoke(() =>
{
    // Your code here
});

When you're in your Thread or Task, simply call this and paste the UI-updating code into it.

My preferred approach is:

  1. Each task/thread updates a separate progress-value
  2. A timer, running on the main thread, averages all the progress values ever X milliseconds and updates the property that the progress bar is bound to.

An advantage of this approach is that the values can be updated in tight loops without risking flooding the message queue with invoke requests.

You could use either the Parallel class or the PLINQ library to process the items in parallel, using multiple ThreadPool threads. For reporting the progress to the UI you could use the IProgress<T> abstraction, where T can be any type of your choice. For example it could be a ValueTuple<string, bool>, in order to communicate both the processed item, and the success/failure of the operation. This way you could create an application-agnostic, library-like method. You could copy-paste this method verbatim in a completely different application (a Console app for example), and it would work exactly the same without any modification. Below is an example of such a method, that uses the PLINQ library for handling the parallelism:

public static string[] ProcessAllItems(string[] items, string baseUrl,
    IProgress<(string, bool)> progress)
{
    return items
        .AsParallel()
        .AsOrdered()
        .WithDegreeOfParallelism(4)
        .Select(item =>
        {
            HttpRequest req = new HttpRequest();
            req.Proxy = null;
            req.ConnectTimeout = 5000;
            req.IgnoreProtocolErrors = true;
            var response = req.Get(baseUrl + url);
            if (response.StatusCode == Leaf.xNet.HttpStatusCode.OK)
            {
                progress.Report((item, true)); // Success
                return item;
            }
            progress.Report((item, false)); // Failure
            return null;
        })
        .Where(result => result != null)
        .ToArray();
}

Then all that you'll have to do is to create a Progress<(string, bool)> object on the UI thread, and pass a delegate that handles the reported messages from the background thread. This delegate should update both the myResults and progressbar UI elements. Calling the ProcessAllItems should be wrapped in an await Task.Run, in order to keep the UI responsive.

private async void startButton_Click(object sender, RoutedEventArgs e)
{
    string baseUrl = myURL.Text;
    string[] items = myLoadedList.Items.Select(x => x.ToString()).ToArray();
    var completedCount = 0;

    var progress = new Progress<(string, bool)>(message =>
    {
        if (message.Item2)
        {
            myResults.Items.Add(message.Item1);
        }
        completedCount++;
        progressbar.Value = completedCount * 100 / items.Length;
    });

    progressbar.Value = 0;
    myResults.Items.Clear();
    myResults.Items.Add("----Starting----");

    string[] results = await Task.Run(() =>
    {
        return ProcessAllItems(items, baseUrl, progress);
    });

    progressbar.Value = 100;
    myResults.Items.Add("----Finish----");
}

Notice the async keyword in the startButton_Click handler, that enables the use of the await operator.

The main point of this suggestion is to avoid using the awkward Dispatcher.Invoke method, that encourages intermingling the processing logic with the presentation logic, as well as the technologically obsolete BackgroundWorker class.

Related