WPF 进度条不更新

5

这是非正式的和原型代码,因此我尝试自己认为应该起作用的方法,如果不行再去查找谷歌,然后在浏览类似问题后在这里询问。

我在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视图上,绑定到StatusMessage的TextBlock会正确更新和显示,但绑定到StatusProgress的ProgressBar则不会更新,保持空白。如果我在事件处理程序中设置断点,我可以看到StatusProgress属性被正确地更新为从0到100的各种值,但这不会反映在ProgressBar上。我意识到事件处理程序在另一个线程上执行,这经常会导致UI更新问题,但为什么一个UI元素能够正确更新而另一个不能呢?
注意: 我已经异常愚蠢并没有静态测试ProgressBar,即只需将viewmodel的StatusProgress设置为一个值并且让shell窗口显示,而不是通过下载循环。如果我这样做,进度条会显示与其Value属性更或多或少相对应的长度。评论或答案中提出的任何布局更改建议都无法改变这一点。静态地它总是可见的,并且总是显示一个值。
示例: 我创建了一个小示例,认为它复制了这个问题。在该示例中,直到等待的任务完成之前,进度条才会更新,我认为这也是我的主要问题所在,但这是一个长时间的下载,我在注意到进度条未更新之前没有等待它完成。
这里是MainWindow.xaml中的StatusBar:
<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完成其循环,然后突然间进度条就显示完整的进度,即下载完成。

1
如果将包含ProgressBar的StatusBarItem的HorizontalContentAlignment设置为“Stretch”,它是否看起来有任何不同? - V.Leon
1
没有 Width 重现了您的问题 - jsanalytics
1
使用 Width 解决你的问题 - jsanalytics
1
由于DataBindEngine可以很好地处理多线程,我认为可能存在一个更简单的问题。也许你应该:
  1. 确认通过ProgressBar.ValueChanged事件是否真正未更改ProgressBar.Value
  2. 如果第1步的结果至少为两次,则确认每次都已更改fileTransferStatusEventArgs.Progress,并且fileTransferStatusEventArgs.Progress具有您期望的值。
  3. 如果第1步的结果为负,则确认访问fileTransferStatusEventArgs.Progress是否会导致任何异常。
- Alex.Wei
1
Width是"missing"时,即使StatusProgress在UI线程中(或已经以其他方式正确处理),也会发生OP描述的问题。而且,正如我们之前建议的那样,为Width分配一个合适的值,可以在StatusProgress在UI线程中更新或不更新的情况下解决这个问题 - jsanalytics
显示剩余14条评论
6个回答

3

正在发生什么

任何与用户界面无关的操作都应该在任务中完成,否则会阻塞用户界面和UI。在你的情况下,下载正发生在用户界面线程上,后者正在等待下载完成后才能更新用户界面。

解决方案

要解决您的问题,您需要做两件事情

  1. 将工作从用户界面线程中移除。

  2. 确保工作可以与用户界面线程通信。

因此,首先将下载工作作为任务启动,如下所示:

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

0

像第一个答案一样,请确保在主 UI 线程上运行,因为 OnFileTransferStatusChanged 在另一个线程上。在你的事件中使用这个。

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

0

我对您的示例进行了一些更改,因为您下载的文件在UI线程上运行,应用程序会冻结,您可以通过将焦点更改为其他应用程序并尝试返回来查看它 - 窗口不会出现也不会更新。

更改:

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

强制下载在新线程中执行。

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

移除了强制转换和访问 UI 线程属性 DataContext。现在我可以看到进度条填充


0

你看不到任何改变,因为你的主线程又称UI线程正在忙于休眠, 没有时间来更新你的用户界面。Main Thread

把你耗时的任务交给Task去处理,把主线程留给更新UI。

将你的代码包装在Task中,你就能看到进度条在前进了。

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

 }

0
这是因为 StatusBarItem 的默认样式将其 HorizontalContentAlignment 设置为 Left,导致 ProgressBar 水平方向只获得了很少的空间。
您可以通过将 StatusBarItemHorizontalContentAlignment 设置为 Stretch 或者设置 ProgressBarWidth 来使其完全填充 StatusBarItem

请查看我的编辑。ProgressBar会显示,而无需设置HorizontalContentAlignment - ProfK
1
是的,因为您现在的编辑包括 Width 属性。我更喜欢在 StatusBarItem 中设置 HorizontalContentAlignmentStretch,而不是在 ProgressBar 中设置静态 Width,因为现在您有两个定义静态宽度的地方(GridColumnProgressBar)。 - V.Leon

0

ProgressBar是DispatcherObject,而DispatcherObject只能由与其关联的Dispatcher访问

如果我理解你的问题正确,你的OnFileTransferStatusChanged正在后台线程上触发,因此,由于你没有使用Dispatcher(或来自UI线程)访问控件,所以不能保证代码将正常工作。

问题在于从非UI线程绑定通常有效,直到它不起作用 - 例如,在非开发机器上。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接