2

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;
    }
}
Vico Lays
  • 53
  • 6
  • 2
    You probably don't need the `BackgroundWorker` if you use Tasks and Invoke. See https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.control.invoke?view=net-5.0 Invoke will prevent the error message you are seeing when trying to update the progress bar from another thread. – Robert Harvey Apr 22 '21 at 14:52
  • 1
    There's a good treatment of this subject [here](https://jeremybytes.blogspot.com/2016/12/incremental-progress-reporting-vs.html). – Robert Harvey Apr 22 '21 at 14:58
  • BackgroundWorker is deprecated. In modern Sharp, it is correct to use asynchronous methods (async / await) and Task. – EldHasp Apr 22 '21 at 15:02
  • 2
    You have created most of the problems for yourself. It is common practice in WPF to use data bindings rather than direct access to UI elements. When using bindings, data can be changed in any thread. – EldHasp Apr 22 '21 at 15:05
  • @RobertHarvey Thanks a lot for your help. I guess, I tried many solutions, but lost myself. Indeed, the answer was in front of me. Thanks a lot for your help again – Vico Lays Apr 22 '21 at 15:23

3 Answers3

3

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.

baltermia
  • 1,151
  • 1
  • 11
  • 26
  • Thanks a lot for your help. I was indeed "blind". I guess, I tried many solution, but lost myself. Indeed, the answer was in front of me. Thanks a lot for your help again. – Vico Lays Apr 22 '21 at 15:22
  • @VicoLays Make sure your dispatcher is not null as your tests will fail with this approach. – XAMlMAX Apr 22 '21 at 16:24
  • note that `Dispatcher.Invoke` "executes the specified delegate *synchronously*". I.e. the worker thread will block until the UI has been updated. – JonasH Apr 23 '21 at 08:30
1

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.

JonasH
  • 28,608
  • 2
  • 10
  • 23
  • This also provides a risk of a memory leak if you use wrong timer and don't dispose of the event handler. – XAMlMAX Apr 22 '21 at 16:23
  • 1
    @XAMIMAX well, yes, you will need to use the tools correctly. I did not think that needed mentioning. – JonasH Apr 23 '21 at 08:28
1

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.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104