WPF ProgressBar 不更新

标签 wpf user-interface events mvvm progress-bar

这是随意的原型(prototype)代码,因此我尝试了我认为应该有效的方法,如果没有,则在谷歌上搜索,然后在仔细阅读类似问题后在这里提问。

我的 Shell 中有以下标记看法:

<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>

然后在 ShellViewModel我有以下两个属性和一个事件处理程序:
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;
}

该事件从文件下载帮助程序类定期引发,即每 n 次迭代。

现在奇怪的是,当事件处理程序更新 vm 属性时,Shell看,TextBlock绑定(bind)到StatusMessage更新和显示正确,但 ProgressBar绑定(bind)到StatusProgress没有,并且保持空白。如果我在事件处理程序中设置一个断点,我可以看到 StatusProgress属性被正确更新为从 0 到 100 的各种值,但这并未反射(reflect)在 ProgressBar 上.

我想到了在另一个线程上执行事件处理程序的想法,这通常会导致 UI 更新问题,但是为什么一个 UI 元素可以正确更新而另一个没有呢?

注意:我一直非常愚蠢,没有测试ProgressBar静态的,即只设置 View 模型的 StatusProgress到一个值并获取要显示的 shell 窗口,而无需通过下载循环。如果我这样做,进度条显示的长度或多或少对应于它的Value。属性(property)。评论或答案中提出的布局更改建议都不会改变这一点。静态地它总是可见的并且总是显示一个值。

示例:我创建了一个相信重复问题的小例子。在示例中,进度条在等待的任务完成之前不会更新,我相信我的主要问题就是这种情况,但下载时间很长,我没有等到它完成才注意到进度条没有更新。

这里是 StatusBar在`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>

使用 MainWindow.xaml.cs 中的代码:
public MainWindow()
{
    InitializeComponent();
    DataContext = new MainWindowViewModel();
}
public MainWindowViewModel ViewModel => (MainWindowViewModel)DataContext;
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
    ViewModel.Download();
}

以及 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();
}

最后是 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);
        }
    }
}

在示例中,进度条保持空白,直到 FileDownloader完成它的循环,然后进度条突然显示完整的进度,即完成。

最佳答案

发生了什么

任何与 UI 无关的事情都应该在任务中完成,因为如果不这样做,您将阻塞 UI 线程和 UI。
在您的情况下,下载发生在您的 UI 线程上,后者在更新您的 UI 之前等待下载完成。

解决方案

你需要做两件事解决您的问题:

  • 从 UI 线程中删除工作。
  • 确保工作可以与您的 UI 线程进行通信。

  • 所以,首先,以 Task 的身份开始下载工作。像这样:
    private ICommand _startDownloadCommand;
    public ICommand StartDownloadCommand
    {
        get
        {
            return _startDownloadCommand ?? (_startDownloadCommand = new DelegateCommand(
                       s => { Task.Run(() => Download()); },
                       s => true));
        }
    }
    

    并将按钮连接到命令,如下所示:
    <Button Command="{Binding StartDownloadCommand}" Content="Start download" Height="20"/>
    

    然后你下载方法是这样的:
    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";  });
    }
    

    调度将从非 UI 线程更新您的属性(在 UI 线程上)。

    然而,DelegateCommand助手类:
    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);
    }
    

    评论

    为了实现 MVVM 模式,我有以下代码:
    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 {}
    

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

    和这个 ViewModel:
    public class MainWindowViewModel: INotifyPropertyChanged, IViewModel
    

    关于WPF ProgressBar 不更新,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50111892/

    相关文章:

    javascript - DOMContentLoaded 仅一次

    JAVA标签设计

    c# - 如何在 WPF 按钮中获取按钮边框和半椭圆样式?

    c# - WPF 虚拟化 Treeview 中的滚动错误

    c# - 附加属性更改事件?

    linux - GTK/Qt On PLT 方案

    mysql - 如何在 Symfony 4 中通过 Doctrine 创建数据库表?

    javascript事件onkeypress事件不会运行

    c# - 将具有多个事件参数的 VB.NET 事件转换为 C#

    c# - 如何通知对象实例之外的属性?