How to create a simple progress bar that updates in real time in WPF?

I have the following code:

var progress = new Progress<int>(valor => progressElement.Value = valor);

await Task.Run(() =>
       {
           Application.Current.Dispatcher.BeginInvoke(new Action(() =>
           {
               var children = LogicalTreeHelper.GetChildren(canvas);
               //count the number of children in a separate variable
               var childaux = LogicalTreeHelper.GetChildren(canvas);
               int numChildren = ((List<object>)childaux.OfType<object>().ToList()).Count();

               progressElement.Maximum = numChildren;

               int childcont = 0;

               foreach (var child in children)
               {
                   //long code work
                   childcont++;
                   ((IProgress<int>)progress).Report(childcont);
                   Thread.Sleep(100);
               }
           }));
       });

The result is, the progresss bar updating at the end of the loop, instead of refreshing in real time. I cant remove BeginInvoke because then i cant access my canvas element. Any help is appreciated.

Upvotes: 0

Views: 579

Answers (4)

Gilad Waisel
Gilad Waisel

Reputation: 205

I attach a working code that I assume is doing what is needed. I added some pieces that I dreamed up. The Handling of the LogicalTreeHelper must run on the UI thread , but is not too heavy. For each "child" I start a task which is the slow part. After the await I update the view model Counter property that is bounded to the ProgressBar.

Code behind:

public partial class MainWindow : Window
    {
        MainViewModel _mainViewModel = new MainViewModel();
        public MainWindow()
        {
            InitializeComponent();
            DataContext = _mainViewModel;
            for(int i =0;i<100;i++)
            {
                Button btn = new Button();
                canvas.Children.Add(btn);
            }
        }
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Start();
        }
        public async void Start()
        {
            var children = LogicalTreeHelper.GetChildren(canvas);
            //count the number of children in a separate variable
            var childaux = LogicalTreeHelper.GetChildren(canvas);
            int numChildren = ((List<object>)childaux.OfType<object>().ToList()).Count();
            _mainViewModel.MaxChildren = numChildren;
            int childcont = 0;
            foreach (var child in children)
            {
                await Task.Run(() =>
                {
                    {
                        //long code work
                        childcont++;
                        Thread.Sleep(100);
                    }
                });
                _mainViewModel.Counter = childcont;
            }  
        }
    }
}

The view model :

public class MainViewModel : INotifyPropertyChanged
    {
        int _counter;
        public int Counter
        {
            get { return _counter; }
            set { _counter = value; NotifyPropertyChanged(nameof(Counter)); }
        }
        int _maxChildren = 1000;
        public int MaxChildren
        {
            get { return _maxChildren; }
            set { _maxChildren = value; NotifyPropertyChanged(nameof(MaxChildren)); }
        }
        public event PropertyChangedEventHandler PropertyChanged;
        protected void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

The XAML in the main window

<Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>


        </Grid.RowDefinitions>
        <Button Grid.Column="0" Click="Button_Click">Start!</Button>
        <ProgressBar Grid.Column="1" Value = "{Binding Counter}"  Margin = "10" Maximum = "{Binding MaxChildren}"  
                  Height = "15" IsIndeterminate = "False" />
        <Canvas  x:Name="canvas" Grid.Row="1"/>

    </Grid> 

Upvotes: 0

Andy
Andy

Reputation: 12276

What you're doing there is starting up a background thread then that just starts up a process on the ui thread, which seems to be very expensive.

Unless this is some sort of much simplified version of your real code, the task is pointless. You might as well just run all that code on the ui thread.

Your big problem there though is

 Thread.Sleep(100);

You're blocking the UI thread which is the thing would render any changes to UI.

If your method was instead async:

       Application.Current.Dispatcher.BeginInvoke(new Action(**async** () 

You could then instead of sleeping the thread do:

 await Task.Delay(100);

Which would free up the ui thread for 100ms

You should not use UI as a data store though. If you instead used mvvm and bound viewmodels which were templated out into UI then you could work with the data in those viewmodels rather than the things in the cannvas.

Then you could likely run your expensive code on a background thread and just dispatch progress changes back to the UI thread. In fact if you bound the value on a progress bar to a property on a viewmodel implements inotifypropertychanged then you would likely find such a simple binding's change notification was automatically marshalled back to the UI thread and you needed no dispatcher code.

Upvotes: 1

JonasH
JonasH

Reputation: 36341

Assuming you are using a view model, bind the progress-bar value to some value, say MyProgress, then update this value using a Progress<T>:

int myProgress;
int MyProgress{
  get => myProgress; 
  set{ 
  myProgress; 
  OnPropertyChanged(nameof(MyProgress));
}
public async void StartSlowMethod(){
  var progress = new Progress<int>();
  progress.ProgressChanged += p => MyProgress = p;
  var result = await Task.Run(() => SlowMethod(progress));
  // Handle the result
}
public double SlowMethod(IProgress<int> progress){
   for(int i = 0; i < 1000; i++){
      progress.Report(i);
      ...
   }
}

Note that this is written on free hand and not tested, there might be some errors I missed. I would recommend checking the official examples.

You will need to ensure the Max-value of the progress bar has the correct value. I usually prefer to report a double from 0-1, and scale this to the progress-range just before displaying. I also prefer to wrap all of this in a helper class to make it easier to start methods on a background thread, and display a progress bar while it is working.

Upvotes: 1

CupKido
CupKido

Reputation: 1

I haven't done this in a while, but I'd recommend using a "background worker" thread, which worked well for me a few times before

Upvotes: 0

Related Questions