ProfK
ProfK

Reputation: 51063

WPF ProgressBar not updating

This is casual and prototype code, hence me trying what I think should work, googling around if it doesn't, then asking here after perusing similar questions.

I have the following markup in my Shell view:

<StatusBarItem Grid.Column="0">
    <TextBlock Text="{Binding StatusMessage}" />
</StatusBarItem>
<Separator Grid.Column="1" />
<StatusBarItem Grid.Column="2">
    <ProgressBar Value="{Binding StatusProgress}" Minimum="0" Maximum="100" Height="16" Width="198" />
</StatusBarItem>

Then in ShellViewModel I have the following two properties and an event handler:

private string _statusMessage;
public string StatusMessage
{
    get => _statusMessage;
    set => SetProperty(ref _statusMessage, value);
}    
private double _statusProgress;
public double StatusProgress
{
    get => _statusProgress;
    set => SetProperty(ref _statusProgress, value);
}

private void OnFileTransferStatusChanged(object sender, FileTransferStatusEventArgs fileTransferStatusEventArgs)
{
    StatusMessage = fileTransferStatusEventArgs.RelativePath;
    StatusProgress = fileTransferStatusEventArgs.Progress;
}

The event is raised periodically, i.e. every n iterations, from a file download helper class.

Now the strange thing is this, when the event handler updates the vm properties, on the Shell view, the TextBlock bound to StatusMessage updates and displays correctly, but the ProgressBar bound to StatusProgress does not, and remains blank. If I put a break-point in the event handler, I can see the StatusProgress property being properly updated in various values from 0 to 100, yet this does not reflect on the ProgressBar.

The idea of the event handler executing on another thread, which often causes UI update problems, occurred to me, but why is one UI element updating properly and the other not?

NOTE: I have been monumentally stupid and not tested the ProgressBar statically, i.e. just set the viewmodel's StatusProgress to a value and get the shell window to display, without going through the download loop. If I do this, the progress bar displays a length that more or less corresponds to its Value property. None of the layout change suggestions made in comments or answers changes this. Statically it is always visible and always displays a value.

EXAMPLE: I created a small example that believe duplicates the problem. In the example the progress bar doesn't update until the waited on task has completed, and I believe this is the case with my main question, but it was a long download, and I didn't wait for it to complete before noticing the progress bar wasn't updating.

Here is the StatusBar in `MainWindow.xaml:

<StatusBar DockPanel.Dock="Bottom" Height="20">
    <StatusBar.ItemsPanel>
        <ItemsPanelTemplate>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="2" />
                    <ColumnDefinition Width="200" />
                </Grid.ColumnDefinitions>
            </Grid>
        </ItemsPanelTemplate>
    </StatusBar.ItemsPanel>
    <StatusBarItem Grid.Column="2">
        <ProgressBar Value="{Binding StatusProgress}" Maximum="100" Minimum="0" Height="16" Width="198" />
    </StatusBarItem>
</StatusBar>

With the code behind in MainWindow.xaml.cs:

public MainWindow()
{
    InitializeComponent();
    DataContext = new MainWindowViewModel();
}
public MainWindowViewModel ViewModel => (MainWindowViewModel)DataContext;
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
    ViewModel.Download();
}

And the code in the MainWindowViewModel:

private string _statusMessage = "Downloading something";
public string StatusMessage
{
    get => _statusMessage;
    set
    {
        if (value == _statusMessage) return;
        _statusMessage = value;
        OnPropertyChanged();
    }
}

private int _statusProgress;
public int StatusProgress
{
    get => _statusProgress;
    set
    {
        if (value == _statusProgress) return;
        _statusProgress = value;
        OnPropertyChanged();
    }
}

public void Download()
{
    var dl = new FileDownloader();
    dl.ProgressChanged += (sender, args) =>
    {
        StatusProgress = args.Progress;
    };
    dl.Download();
}

And finally the code for FileDownloader:

public class ProgressChangedEventArgs
{
    public int Progress { get; set; }
}

public class FileDownloader
{
    public event EventHandler<ProgressChangedEventArgs> ProgressChanged;
    public void Download()
    {            
        for (var i = 0; i < 100; i++)
        {
            ProgressChanged?.Invoke(this, new ProgressChangedEventArgs{Progress = i});
            Thread.Sleep(200);
        }
    }
}

In the example, the progress bar remains blank, until FileDownloader finishes its loop, and then suddenly the progress bar shows full progress, i.e. complete.

Upvotes: 6

Views: 9062

Answers (6)

Soleil
Soleil

Reputation: 7287

What's happening

Anything that is not about UI should be done in tasks, because, if not, you're blocking the UI thread and the UI. In your case, the download was happening on you UI thread, the latter was waiting for the download to finish before updating your UI.

Solution

You need to do two things to solve your problem:

  1. remove the work from the UI thread.

  2. make sure the work can communicate with you UI thread.

So, first, start the download work as a Task like this:

private ICommand _startDownloadCommand;
public ICommand StartDownloadCommand
{
    get
    {
        return _startDownloadCommand ?? (_startDownloadCommand = new DelegateCommand(
                   s => { Task.Run(() => Download()); },
                   s => true));
    }
}

and connect the button to the command like this:

<Button Command="{Binding StartDownloadCommand}" Content="Start download" Height="20"/>

Then have you download method as such:

public void Download()
{
    Application.Current.Dispatcher.Invoke(() => { StatusMessage = "download started";  });

    var dl = new FileDownloader();
    dl.ProgressChanged += (sender, args) =>
    {
        Application.Current.Dispatcher.Invoke(() => { StatusProgress = args.Progress; });
    };
    dl.Download();

    Application.Current.Dispatcher.Invoke(() => { StatusMessage = "download DONE";  });
}

The dispatch will have your property (on UI thread) updated from a non UI thread.

And yet, the DelegateCommand helper class:

public class DelegateCommand : ICommand
{
    private readonly Predicate<object> _canExecute;
    private readonly Action<object> _execute;
    public event EventHandler CanExecuteChanged;

    public DelegateCommand(Action<object> execute)
        : this(execute, null) {}

    public DelegateCommand(Action<object> execute,
        Predicate<object> canExecute)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter);
    public void Execute(object parameter) => _execute(parameter);
    public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

Remarks

In order to implement the MVVM pattern I had this code behind:

public partial class MainWindow : IView
{
    public IViewModel ViewModel
    {
        get { return (IViewModel)DataContext; }
        set { DataContext = value; }
    }

    public MainWindow()
    {
        DataContext = new MainWindowViewModel();
    }
}

public interface IViewModel {}

public interface IView {}

and this View:

<Window x:Class="WpfApp1.MainWindow"
        d:DataContext="{d:DesignInstance local:MainWindowViewModel,
            IsDesignTimeCreatable=True}"
        xmlns:local="clr-namespace:WpfApp1"

and this ViewModel:

public class MainWindowViewModel: INotifyPropertyChanged, IViewModel

Upvotes: 4

tabby
tabby

Reputation: 1918

You can't see any changes because your Main thread AKA UI Thread is busy Sleeping and it does not have time to update your UI Main Thread

Let Task handle your lengthy job and Main thread for Updating UI

Wrap your code inside Task and you can see your progress bar progressing.

private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
      await Task.Run(() =>
      {
           _viewModel.Download();
       });
            //_viewModel.Download(); //this will run on UI Thread

 }

Upvotes: 1

Rafal
Rafal

Reputation: 12619

I made few changes to your sample as your file downloaded is working on UI thread and application just freezes you can see it by changing focus to other application and trying to get back - window will not appear nor update.

changes:

 private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
 {
    Task.Factory.StartNew(() => ViewModel.Download());
 }

forces download to execute in new thread.

public MainWindow()
{
    InitializeComponent();
    DataContext = ViewModel = new MainWindowViewModel();
}
public MainWindowViewModel ViewModel { get; }

removed cast and access to UI thread only property DataContext. Now I can see progress bar filling up.

Upvotes: 1

GCamel
GCamel

Reputation: 622

like first answer be sure to be on the main UI thread, because OnFileTransferStatusChanged is on another thread. use this in your event

    Application.Current.Dispatcher.Invoke(prio, (ThreadStart)(() => 
{
   StatusMessage = fileTransferStatusEventArgs.RelativePath;
    StatusProgress = fileTransferStatusEventArgs.Progress;
}));

Upvotes: 0

Maciek Świszczowski
Maciek Świszczowski

Reputation: 1175

ProgressBar is a DispatcherObject, and DispatcherObject can be only accessed by the Dispatcher it is associated with.

If I understand your question well your OnFileTransferStatusChanged is being triggered on a background thread, so since you're not accessing controls using a Dispatcher (or from the UI thread) you're not guaranteed that the code will work.

The problem is that binding from a non-UI thread usually works until it doesn't - e.g. on a non-dev machine.

Upvotes: 0

V.Leon
V.Leon

Reputation: 586

This happens, because StatusBarItem default style sets its HorizontalContentAlignment to Left, which leads ProgressBar to get only a small amount of space horizontally.

You can make the ProgressBar to fill the StatusBarItem completely by setting StatusBarItem's HorizontalContentAlignment to Stretch or you can set the Width of the ProgressBar.

Upvotes: 0

Related Questions