9 Replies - 735 Views - Last Post: 26 July 2021 - 09:51 AM Rate Topic: -----

#1 ScottinTexas   User is offline

  • D.I.C Regular

Reputation: 17
  • View blog
  • Posts: 361
  • Joined: 13-March 12

Updating the UI from within a long running loop

Posted 10 June 2021 - 12:10 PM

I have a loop running gathering a lot of data. I want to use a progress bar and a status text to let the user know what is going on, but the UI wont update from inside the loop. And I don't know how to use a background worker and keep an outer loop from continuing once the background worker goes to work.

The following loop would run but I couldn't get a progress bar to work with this loop. The UI would not update. The Status text never changed. The progress bar and status text are properties and the UI is bound to them. And yes, I am using RaisePropertyChanged();

This is the outer loop
for (int i = 0; i < FileCount; i++)
{
    ProgressBarValue = 0;
    PBarMax = 60;
    string nxt = GetNextFileName(lfn);
    //If the file exists then we need to read it.
    if (FTPFileExists(nxt.Split('\\').Last().Trim()))
    {
        //Stop the background worker so it doesn't fire in the middle of a long data upload.
        EggTimer.CancelAsync();
        StatusText = "Unread files exist on the server.";
        //This can take a really long time! But the UI needs to get updated so I put the gathering
        //in a BackgroundWorker so I could free up the UI. 
        DataGatherer.RunWorkerAsync(argument: nxt);
    }
}




But this has the problem os getting the UI started and then looping to the next file (if there is one) and then we have a disaster.

I am including the code for the BackgroundWorker but the whole question is wrapped up above.

Private void RunDataGatherer()
{;
    DataGatherer.WorkerSupportsCancellation = true;
    DataGatherer.WorkerReportsProgress = true;

    DataGatherer.ProgressChanged += (s,e)=>
    {
        ProgressBarValue = e.ProgressPercentage;
        StatusText = (string)e.UserState;
    };

    DataGatherer.DoWork += (s,e) =>
    {
        string nxtFile = (string)e.Argument;
        string dataString = GetDailyData(nxtFile);
        e.Result = dataString;
    };

    DataGatherer.RunWorkerCompleted += (s,e)=>
    {
        string dataString=e.Result as string;
        //Do the rest of the stuff
    }
}



How can I use the progress bar and prompts while this long running loop is going without using a background worker? Or how do I use a background worker but keep the outer loop from looping while the background worker is working? Or is there a super simple solution that I am missing here? Seems that happens all of the time!

Thanks for checking this out. And thanks more for your response.

Is This A Good Question/Topic? 0
  • +

Replies To: Updating the UI from within a long running loop

#2 modi123_1   User is online

  • Suitor #2
  • member icon



Reputation: 16302
  • View blog
  • Posts: 64,852
  • Joined: 12-June 08

Re: Updating the UI from within a long running loop

Posted 10 June 2021 - 12:45 PM

I'm always a fan of 'Tasks' instead of background workers.

Say you have a winform with two buttons and two textboxes. You want one to update textbox1 with a value from a loop, but still have the UI free to click button2 to add text to textbox2.


If you do something like this, the UI is locked up until that button1 click event finishes.
private void button1_Click(object sender, EventArgs e)
        {
            for (var i = 0; i <= 5000; i++)
            {
                    textBox1.AppendText($"{i}{Environment.NewLine}");
            }

        }

        private void button2_Click(object sender, EventArgs e)
        {
            textBox2.AppendText($"here {Environment.NewLine}");
        }



Instead you wrap the for loop in a Task delegate, and kick the 'Invoke' actoin off.

        private async void button1_ClickAsync(object sender, EventArgs e)
        {
            await Task.Run(() =>
            {
                for (var i = 0; i <= 5000; i++)
                {
                    Invoke((Action)(() =>
                    {
                        textBox1.AppendText($"{i}{Environment.NewLine}");
                    }));
                }
            });
        }


Now you are free to mash that button2 to your heart's content.

You can use a similar thing to update a progress bar or what ever.
Was This Post Helpful? 0
  • +
  • -

#3 Skydiver   User is online

  • Code herder
  • member icon

Reputation: 7816
  • View blog
  • Posts: 26,102
  • Joined: 05-May 12

Re: Updating the UI from within a long running loop

Posted 10 June 2021 - 06:55 PM

I'm assuming that you are trying to do this in WinForms because of your leaning towards using BackgroundWorker instead of using Task.

Anyway, in WinForms, the reason why the StatusText doesn't update is because you never actually yield the UI thread control back to the UI thread. The UI thread is still stuck in your for loop, until the loop is over. The UI thread needs a chance to process the messages including paint messages so that the status text value can actually be painted to the screen.

With WPF, although the way rendering is done, it still generally the same problem. You still need to let the UI thread have a chance to update the screen. You can't hog the CPU.

The reason why the code in post #2 works is because all the work is done in another thread, not the UI thread, and the UI just to update via the calls to Invoke(). What happens when Invoke() is called in WinForms is that a message is posted into the UI thread's message queue indicating that a control need to handle a message. When the UI thread's message pump encounters that posted message, it calls the control's message handler. The message handler decodes the posted message and determines that something needs to be updated in the UI. Typically as part of a UI update, the part of the control or windows is marked as invalid, and a paint message is posted. Later when the UI threads message pump encounters the paint message, it tells the control to repaint the invalid portion of the control or window. A similar mechanism happens in WPF.
Was This Post Helpful? 0
  • +
  • -

#4 ScottinTexas   User is offline

  • D.I.C Regular

Reputation: 17
  • View blog
  • Posts: 361
  • Joined: 13-March 12

Re: Updating the UI from within a long running loop

Posted 11 June 2021 - 03:50 AM

View PostSkydiver, on 10 June 2021 - 07:55 PM, said:

I'm assuming that you are trying to do this in WinForms because of your leaning towards using BackgroundWorker instead of using Task.

No. Definitely not WinForms. This is WPF, MVVM desktop application. The views are the typical Window class built with XAML.

I understand the problem behind hogging the CPU which prevents the UI from updating. That is why I used a background worker to get the work onto a different thread and still have the progress bar and status text updated using the ReportProgress method. I didn't want to add the outer loop onto the thread as well (I am not clear why), so I was looking for a way to stop the out loop while the inner loop did it's thing.

Why is a Background Worker a bad thing?
Was This Post Helpful? 0
  • +
  • -

#5 Skydiver   User is online

  • Code herder
  • member icon

Reputation: 7816
  • View blog
  • Posts: 26,102
  • Joined: 05-May 12

Re: Updating the UI from within a long running loop

Posted 11 June 2021 - 06:43 AM

It's not necessary bad. It's just got an impedance mismatch with the way async/await is used extensively within WPF.
Was This Post Helpful? 0
  • +
  • -

#6 ScottinTexas   User is offline

  • D.I.C Regular

Reputation: 17
  • View blog
  • Posts: 361
  • Joined: 13-March 12

Re: Updating the UI from within a long running loop

Posted 11 June 2021 - 07:22 AM

While Task is running, how do you report it's progress? Suppose you want the user to see that it really is doing something. Say it is 10% finished uploading 73465 lines of data. How does that information come from the task to the progress bar? With a background worker you have ReportProgress.
Was This Post Helpful? 0
  • +
  • -

#7 Skydiver   User is online

  • Code herder
  • member icon

Reputation: 7816
  • View blog
  • Posts: 26,102
  • Joined: 05-May 12

Re: Updating the UI from within a long running loop

Posted 11 June 2021 - 07:55 AM

Your progress bar and status text should be bound to a view model. Update the property in the view model that shows the progress or status. You'll likely have to pass those updates as lambdas into Dispatcher.Invoke() so that the appropriate crossthread things happen correctly.
Was This Post Helpful? 0
  • +
  • -

#8 ScottinTexas   User is offline

  • D.I.C Regular

Reputation: 17
  • View blog
  • Posts: 361
  • Joined: 13-March 12

Re: Updating the UI from within a long running loop

Posted 17 June 2021 - 10:23 AM

View PostSkydiver, on 11 June 2021 - 09:55 AM, said:

Your progress bar and status text should be bound to a view model.


It is.
        private void DataGathererReporting(object sender, ProgressChangedEventArgs e)
        {
            ProgressBarValue = e.ProgressPercentage;
            StatusText = (string)e.UserState;
        }



ProgressBarValue property is bound to a ProgressBar in the MainWindowView and StatusText property is bound to a textbock in the MainWindowView. This is how I always do it and that is why, when this thread started, I had a BackgroundWorker running that reported progress. But what I need is (from what I gather from reading and the clues given me in previous responses) is an async/await scenario. Because I need to WAIT for the long running stuff to finish before going to the next step in the process, BUT I need to update the UI while the long running stuff is going on. So I am trying to understand the async/await stuff and every example I look at just waits for something to finish. It doesn't return a value, it doesn't update the UI. So far anyway. I need to find more references.

What I have tried is; in the MainWIndowViewModel I have a timer set up to wait for n minutes then check an FTP site for a new file and, if found, deal with it. With Timer.Elaspsed I execute the check method, if the file is there, do the long lasting work. Meanwhile keep the UI updated so the user knows something is happening. I also have to restart the timer, but not while the data gathering is in progress. Otherwise it would start running the same gathering method for the same file. But I am not understanding how to set up my async method properly. This is what I have so far.

private void OnTimerElapsed(object sender, ElapsedEventArgs e)
{
    CheckForUnreadFiles();
}


public async Task<string> CheckForUnreadFiles()
{
    //UI stuff
    StatusText = TimeStamp + "Checking for unread files.";
    
    if (CheckForFileUpload())
    {
        string nxt = GetNextFileName(LastFileName);
        string dataString = string.Empty;
        if (FTPFileExists(nxt.Split('\\').Last().Trim()))
        {
            //This can take a while! The timer should not start again until the long running task is complete. 
            timer.Enabled = false;
            await GetDailyData(nxt);
            timer.Enabled = true;
        }
    }

}

public Task GetDailyData(string fileName)
{
    string output = string.Empty;
    if (fileName != LastFileName)
    {
        try
        {
            //Do a lot of work
            //UI stuff
            //MainWindowViewModel Property Legend bound to a textblock
            //MainWindowViewModel Property ProgressBarValue bound to a ProgressBar
            //MainWindowViewModel Property StatusText bound to a textblock
            output=ReadTheData(fileName)
            AppendNewDataToFile(output);
            AddToFilesRead(fileName);
            WriteLastFileName(fileName);
            DeleteFileFromServer(fileName);
        }
        catch (Exception ex)
        {
            MyException rx = new MyException("Exception thrown in GetDailyData." + ex.Message);
            throw rx;
        }
    }
    return null;
}



I am getting a warning at the call to CheckForUnreadFiles that says "Because this call is not awaited, execution of the current method continues before the call is completed." So I don't have the syntax right or something. But I don't need to await that (as far as I can tell) because it doesn't matter if it continues while data is being gathered. As long as the timer doesn't trigger the Elapsed event, everything should be fine.


Thanks.
Was This Post Helpful? 0
  • +
  • -

#9 Sheepings   User is offline

  • D.I.C Lover
  • member icon

Reputation: 246
  • View blog
  • Posts: 1,341
  • Joined: 05-December 13

Re: Updating the UI from within a long running loop

Posted 03 July 2021 - 02:11 PM

Late to the party on this one. Since you are not showing the relevant declarations, models - properties etc, it is hard to believe you when you say :

Quote

ProgressBarValue property is bound to a ProgressBar in the MainWindowView


If that is what you are doing, then you are not working with the mvvm pattern which I assume you want to do? Doing what you describe would also make your datacontext not work at all. Regardless, you are not using any accepted WPF pattern correctly. You certainly are not using WPF properly.


You can not reference a property inside the class of the window you want to update and try to bind directly to that property object. You are missing the whole point of how WPF works with regards to binding.

What you should have are separate classes for your models, an abstract base class for your observer pattern, and your view models. These are not the same things as you are describing here. Until we can actually see the code for our selves, there is no way we can take your word as fact of what you are actually doing. That is providing you still need help with it?

Quote

I am getting a warning at the call to CheckForUnreadFiles


Yes because of this : public async and what is up with line 20 awaiting?
Was This Post Helpful? 0
  • +
  • -

#10 ScottinTexas   User is offline

  • D.I.C Regular

Reputation: 17
  • View blog
  • Posts: 361
  • Joined: 13-March 12

Re: Updating the UI from within a long running loop

Posted 26 July 2021 - 09:51 AM

Hi Sheepings. I was trying to be brief just to make the point. The View has a ProgressBar bound to the MainwindowViewModel property (Value="{Binding ProgressBarValue}").
                <ProgressBar Grid.Row="0"
                    Grid.Column="1"
                    Margin="2"
                    Minimum="0"
                    Maximum="{Binding PBarMax}"
                    HorizontalAlignment="Stretch"
                    Value="{Binding ProgressBarValue}"/>



What relevant declarations do you need? The declarations would come from knowing the solution to the question. I know what a background worker is. I use it all of the time. I have never used Async/Await. If I have a loop running in Async/Await, how can I update the UI with information from within the Async method? Can I just update the properties in the class without a cross threading exception? Once a thread is kicked off, it is off of the current thread and trying to update the current thread from another thread is bad wrong (bdong!). Background thread gives you a way out by using the report progress method. And while it is happily being busy in the background, the user is being kept updated.

The original post has slightly morphed into a more general discussion of the use of Async/Await vs Background Worker. And when someone says why use Background Worker when Async/Await is used so much, I'm thinking maybe I should look into this.
Was This Post Helpful? 0
  • +
  • -

Page 1 of 1