Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 635
ProfK Avatar asked Oct 16 '22 19:10

ProfK


1 Answers

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
like image 75
Soleil Avatar answered Nov 10 '22 19:11

Soleil