WPF ProgressBar не обновляется - PullRequest
0 голосов
/ 01 мая 2018

Это случайный и прототипный код, поэтому я пробовал то, что, по моему мнению, должно работать, гуглял, если не получилось, а затем задавал здесь после просмотра похожих вопросов.

В моем представлении 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, связанный с StatusMessage, обновляется и отображается правильно, но ProgressBar, связанный с StatusProgress нет и остается пустым. Если я установлю точку останова в обработчике событий, я увижу, что свойство StatusProgress правильно обновляется в различных значениях от 0 до 100, но это не отражается на ProgressBar.

Мне пришла в голову идея выполнения обработчика событий в другом потоке, который часто вызывает проблемы с обновлением пользовательского интерфейса, но почему один элемент пользовательского интерфейса обновляется правильно, а другой - нет?

ПРИМЕЧАНИЕ: Я был монументально глупо и не тестировал ProgressBar статически, то есть просто установите StatusProgress модели представления в значение и получите окно оболочки для отображения, не пройдя через цикл загрузки. Если я это сделаю, индикатор выполнения покажет длину, которая более или менее соответствует его свойству Value. Ни одно из предложений по изменению макета, сделанных в комментариях или ответах, не меняет этого. Статически оно всегда видно и всегда отображает значение.

ПРИМЕР: Я создал небольшой пример, который, кажется, дублирует проблему. В этом примере индикатор выполнения не обновляется до тех пор, пока не будет выполнено ожидаемое задание, и я полагаю, что это касается моего основного вопроса, но это была долгая загрузка, и я не дождался ее завершения, прежде чем заметил индикатор выполнения не обновлялся.

Вот 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 не завершит свой цикл, а затем индикатор выполнения не показывает полный прогресс, т.е. завершен.

Ответы [ 6 ]

0 голосов
/ 09 мая 2018

Что происходит

Все, что не связано с пользовательским интерфейсом, должно выполняться в задачах, потому что, если нет, вы блокируете поток пользовательского интерфейса и пользовательский интерфейс. В вашем случае загрузка происходила в вашем потоке пользовательского интерфейса, последний ждал завершения загрузки перед обновлением вашего пользовательского интерфейса.

Решение

Вам нужно сделать две вещи , чтобы решить вашу проблему:

  1. удалить работу из потока пользовательского интерфейса.

  2. убедитесь, что работа может общаться с вами потоком пользовательского интерфейса.

Итак, сначала начните загрузку как 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";  });
}

Для отправки будет обновлено ваше свойство (в потоке пользовательского интерфейса) из потока без пользовательского интерфейса .

И все же, класс помощника 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 голосов
/ 09 мая 2018

Вы не можете видеть никаких изменений, потому что ваш главный поток AKA Пользовательский интерфейс занят Спящий и он не успевает обновить ваш интерфейс Main Thread

Пусть Task обрабатывает вашу длительную работу и основной поток для обновления пользовательского интерфейса

Оберните ваш код в Task, и вы увидите, как прогрессирует индикатор выполнения.

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

 }
0 голосов
/ 09 мая 2018

Я сделал несколько изменений в вашем образце, поскольку загруженный файл работает в потоке пользовательского интерфейса, и приложение просто зависает, вы можете увидеть его, переключив фокус на другое приложение и попытавшись вернуться назад - окно не появится и не обновится.

Изменения:

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

принудительно запускает загрузку в новом потоке.

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

удалено приведение и доступно только свойство пользовательского интерфейса DataContext. Теперь я вижу индикатор прогресса , заполняющий .

0 голосов
/ 04 мая 2018

как первый ответ быть обязательно в основном потоке пользовательского интерфейса, потому что OnFileTransferStatusChanged находится в другом потоке. используйте это в вашем мероприятии

    Application.Current.Dispatcher.Invoke(prio, (ThreadStart)(() => 
{
   StatusMessage = fileTransferStatusEventArgs.RelativePath;
    StatusProgress = fileTransferStatusEventArgs.Progress;
}));
0 голосов
/ 03 мая 2018

ProgressBar является DispatcherObject, а DispatcherObject может быть доступен только Диспетчеру, с которым он связан .

Если я правильно понимаю ваш вопрос, ваш OnFileTransferStatusChanged запускается в фоновом потоке, поэтому, поскольку вы не получаете доступ к элементам управления с помощью Dispatcher (или из потока пользовательского интерфейса), вы не гарантируете, что код будет работать.

Проблема в том, что привязка из не-пользовательского интерфейса обычно работает, пока не работает, например на машине без разработчика.

0 голосов
/ 02 мая 2018

Это происходит потому, что StatusBarItem стиль по умолчанию устанавливает HorizontalContentAlignment на Left, что приводит к ProgressBar, чтобы получить только небольшое количество пространства по горизонтали.

Вы можете сделать ProgressBar для полного заполнения StatusBarItem, установив StatusBarItem s HorizontalContentAlignment на Stretch или вы можете установить Width для ProgressBar.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...