Erik Thysell
Erik Thysell

Reputation: 1352

How to update listbox with progress bar GUI during execution

I have a listbox in a wpf window thats bound to a list in a viewmodel object. When I run a method in the viewmodel object it processes members of the list and each member has a progress. I would like to have the gui update continuously during execution. As it is now, it only updates gui when the processing is finished.

Here I have tried to create a small example of what I have right now:

MainWindow.xaml:

<Window x:Class="WPF_MVVM_Thread_Progressbar.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WPF_MVVM_Thread_Progressbar"
        Title="MainWindow" Height="350" Width="525">
  <Window.DataContext>
    <local:TestViewModel/>
  </Window.DataContext>
    <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="8*"/>
      <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
      <RowDefinition Height="8*"/>
      <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <ListBox Grid.Column="0"  Grid.Row="0" Margin="5" ItemsSource="{Binding TestWorker.TestList}">
      <ListBox.ItemContainerStyle>
        <Style TargetType="{x:Type ListBoxItem}">
          <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
        </Style>
      </ListBox.ItemContainerStyle>
      <ListBox.ItemTemplate>
        <DataTemplate>
          <Grid>
            <ProgressBar Minimum="0" Maximum="100" Value="{Binding Progress, Mode=OneWay}" Background="Bisque">
              <ProgressBar.Style>
                <Style TargetType="{x:Type ProgressBar}">
                  <Style.Triggers>
                    <DataTrigger Binding="{Binding Progress}" Value="0">
                      <Setter Property="Visibility" Value="Hidden"/>
                    </DataTrigger>
                  </Style.Triggers>
                </Style>
              </ProgressBar.Style>
            </ProgressBar>
            <TextBlock FontWeight="Bold" Text="{Binding Path=Name}" Background="Transparent"/>
          </Grid>
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>



    <Button Grid.Column="0" Grid.Row="1" Content="TestRun" Command="{Binding TestRunCommand}"></Button>
    <TextBlock Text="{Binding SelectedIdx}" Grid.Column="1" Grid.Row="1"/>
  </Grid>
</Window>

MainWindowl.xaml.cs:

using Prism.Commands;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;

namespace WPF_MVVM_Thread_Progressbar
{
  /// <summary>
  /// Interaction logic for MainWindow.xaml
  /// </summary>
  public partial class MainWindow : Window
  {
    public MainWindow()
    {
      InitializeComponent();
    }
  }

  public class TestViewModel : INotifyPropertyChanged
  {
    private WorkingClass _testWorker;

    private DelegateCommand _testRunCommand;

    public DelegateCommand TestRunCommand
    {
      get { return _testRunCommand; }
      set { _testRunCommand = value; }
    }

    public WorkingClass TestWorker
    {
      get { return _testWorker; }
      set { _testWorker = value; RaisePropertyChanged("TestWork"); }
    }

    private int _selectedIdx;

    public int SelectedIdx
    {
      get { return _selectedIdx; }
      set { _selectedIdx = value; RaisePropertyChanged("SelectedIdx"); }
    }


    public TestViewModel()
    {
      _testWorker = new WorkingClass();
      _testRunCommand = new DelegateCommand(TestRun, canRun);
    }

    public async void TestRun()
    {
      //await Task.Run(() => _testWorker.Work());
      _testWorker.Work();
    }

    private bool canRun()
    {
      return true;
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void RaisePropertyChanged(string propertyName)
    {
      PropertyChangedEventHandler handler = PropertyChanged;
      if (handler != null)
      {
        handler(this, new PropertyChangedEventArgs(propertyName));
      }
    }

  }

  public class WorkingClass : INotifyPropertyChanged
  {
    private ObservableCollection<TestObject> _testList;

    public ObservableCollection<TestObject> TestList
    {
      get { return _testList; }
      set { _testList = value; RaisePropertyChanged("TestList"); }
    }

    public WorkingClass()
    {
      _testList = new ObservableCollection<TestObject>();
      _testList.Add(new TestObject("Object A"));
      _testList.Add(new TestObject("Object B"));
      _testList.Add(new TestObject("Object C"));
      RaisePropertyChanged("TestList");

    }

    public void Work()
    {
      foreach (var obj in TestList)
      {
        obj.TestWork();
      }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void RaisePropertyChanged(string propertyName)
    {
      PropertyChangedEventHandler handler = PropertyChanged;
      if (handler != null)
      {
        handler(this, new PropertyChangedEventArgs(propertyName));
      }
    }

  }

  public class TestObject : INotifyPropertyChanged
  {
    private string _name;

    public string Name
    {
      get { return _name; }
      set { _name = value; }
    }

    private int _progress;

    public int Progress
    {
      get { return _progress; }
      set { _progress = value; RaisePropertyChanged("Progress"); }
    }

    public TestObject(string name)
    {
      this._name = name;
      _progress = 0;
    }

    public void TestWork()
    {
      for (int i = 0; i < 100; i++)
      {
        System.Threading.Thread.Sleep(10);
        Progress++;
      }
    }


    public event PropertyChangedEventHandler PropertyChanged;
    protected void RaisePropertyChanged(string propertyName)
    {
      PropertyChangedEventHandler handler = PropertyChanged;
      if (handler != null)
      {
        handler(this, new PropertyChangedEventArgs(propertyName));
      }
    }
  }
}

I have tried to use ObservableCollection and INotifyPropertyChanged but this it seems not to be enough.

Eventually I would like to be able to have the same effect using async/await call from the TestViewModel.TestRun().

Could someone perhaps offer some insights on this? It would be much appreciated.

Upvotes: 0

Views: 947

Answers (2)

Lauraducky
Lauraducky

Reputation: 672

I've successfully done this in the past using a BackgroundWorker.

public class TestObject : INotifyPropertyChanged {
    private BackgroundWorker worker;

    public TestObject() {
        worker = new BackgroundWorker() {
            WorkerReportsProgress = true
        };
        worker.DoWork += DoWork;
        worker.ProgressChanged += WorkProgress;
        worker.RunWorkerCompleted += WorkFinished;
    }

    public int Progress
    {
      get { return _progress; }
      set { _progress = value; RaisePropertyChanged("Progress"); }
    }

    // Begin doing work
    public void TestWork() {
        worker.RunWorkerAsync();
    }

    private void DoWork(object sender, DoWorkEventArgs eventArgs) {
        worker.ReportProgress(0, "Work started");

        for (int i = 0; i < 100; i++) {
            System.Threading.Thread.Sleep(10);
            worker.ReportProgress(i, "Message");
        }
    }

    // Fires when the progress of a job changes.
    private void WorkProgress(object sender, ProgressChangedEventArgs e) {
        // Do something with the progress here
        Progress = e.ProgressPercentage;
    }

    // Fires when a job finishes.
    private void WorkFinished(object sender, RunWorkerCompletedEventArgs e) {
        // The work finished. Do something?
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void RaisePropertyChanged(string propertyName)
    {
      // NOTE: If you're running C#6 use the null conditional operator for this check.
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(e));
    }
}

A BackgroundWorker basically runs everything on a separate thread and reports back when its progress changes or it finishes working. You can pull out the ProgressPercentage from its progress report and use that in the UI. Hope that helps. To keep the example simple I didn't include some of your code but that should given you an idea of how it can be done.

Upvotes: 0

netniV
netniV

Reputation: 2418

I think the current reason that you have the UI only updating once completed, is that you are running all of this on the UI thread. I would instead try this:

Task.Run(async delegate
{ 
   await _testWorker.Work();
});

Or

Task.Run(() =>
{ 
    _testWorker.Work();
});

Or

Task.Factory.StartNew(() =>
{ 
    _testWorker.Work();
});

Or

var newThread = new Thread(new ThreadStart(_testWorker.Work());
newThread.Start();

This will return back to the UI instantly but allow your code to continue.

Note: You will have to be careful about the use of objects off the UI thread. ObservableCollections can only be created on the same thread as the dispatcher that handles the UI work. If you are using two-way binding, again you have to be careful about thread safety.

Upvotes: 1

Related Questions