WPF ProgressBar не обновляется

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

В моем представлении 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;
}

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

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

Будет ли это выглядеть иначе, если вы установите HorizontalContentAlignment = "Stretch" на StatusBarItem, содержащий ProgressBar?

V.Leon 01.05.2018 09:36

Что за разметка для строки состояния? В нем должна быть сетка, но какие столбцы в ней есть? Если вы временно поместите прямоугольник с красной заливкой в ​​Grid.Column = "2", это будет видно. Возможно, вам нужно установить ширину панели прогресса или столбца, в котором она находится.

Andy 01.05.2018 09:58

Я бы внимательно посмотрел на проблемы с потоками, даже если статус сообщения действительно отображается. Например, событие может быть сначала вызвано в потоке пользовательского интерфейса (т.е. вызывается event.Invoke ("myfile.txt", 0)). Последующие вызовы могут быть из потока, не относящегося к пользовательскому интерфейсу. Когда поток, не связанный с пользовательским интерфейсом, вызывает это событие, SetProperty сначала устанавливает ваше личное поле. Итак, в ViewModel поле обновляется. Тогда будущие чтения свойств покажут ожидаемое значение. Но SetProperty молча терпит неудачу, когда вызывает привязку, которая является однопоточной.

mwwaters 01.05.2018 12:38

Кстати, я бы порекомендовал посмотреть на await / async и другие инструменты многопоточности. Задача Task.Run может быть сложной для гарантии того, что обратные вызовы происходят в незаблокированном потоке пользовательского интерфейса. С помощью await / async все приложение может концептуально быть однопоточным, при этом единственный поток запускается с того места, где он оставался после завершения ожидания.

mwwaters 01.05.2018 12:43

Mode = TwoWay на индикаторе выполнения? Незачем. Ничего не пахнет, поэтому я бы рекомендовал зацепиться за Snoop, чтобы проверить ваши привязки во время выполнения.

user1228 01.05.2018 19:23

@mwwaters Я использую async, но не другие инструменты для работы с потоками. Это потому, что вся моя инфраструктура построена на использовании HttpClient, который имеет только методы async comms.

ProfK 02.05.2018 05:37

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

ProfK 02.05.2018 05:44

@ Буду ли я всегда - я не так часто делаю XAML - забываю, в каком направлении находится привязка по умолчанию, поэтому я просто ударил Mode=TwoWay как одну из многих вещей, которые я пытался заставить работать индикатор выполнения. Я посмотрю со Снупом, спасибо.

ProfK 02.05.2018 05:47

Свойства INPC можно изменить из любого потока. Класс Binding будет маршалировать обновление за вас в поток пользовательского интерфейса. Я действительно не вижу здесь ничего плохого. Если вы видите, что число меняется, это говорит о том, что вы не наказываете пользовательский интерфейс - частую причину этой проблемы. Может быть, fileTransferStatusEventArgs.Progress всегда равен 0? Я бы прошел через весь процесс, убедился, что он не равен 0, проверил, что свойство читается правильно, затем использовал Snoop для проверки состояния индикатора выполнения, значения inc, min / max, ширины и высоты, макета и т. д. Сделайте 0 предположений, потому что происходит что-то странное.

user1228 02.05.2018 16:39

Я не могу воспроизвести проблему. Вы уверены, что все настроено правильно, т.е. StatusProgress принимает значения в диапазоне от 0 до 100, а не от 0 до 1 (это объясняет тонкую зеленую линию слева)?

Grx70 03.05.2018 09:17

предоставьте XAML, окружающий приведенный выше образец, чтобы помочь

earloc 03.05.2018 12:20

Укажите ширину индикатора выполнения: <ProgressBar Value = "{Binding StatusProgress, Mode=TwoWay}" Height = "16" Width = "100"/>.

jsanalytics 03.05.2018 14:25

Нет Widthвоспроизводит вашу проблему.

jsanalytics 03.05.2018 14:47

С Widthрешает вашу проблему.

jsanalytics 03.05.2018 14:48

Мне кажется, он чист. Я не могу понять это, но если кажется, что StatusProgress обновляется должным образом, то мне придется винить уведомление пользовательского интерфейса. Попробуйте добавить (да, избыточный и урезанный (в зависимости от фреймворка)) вызов к OnPropertyChanged(). Если он ничего не меняет, значит, вы исключили почти все, кроме проблемы с потоками.

Adam Vincent 03.05.2018 21:28

Поскольку DataBindEngine отлично работает с многопоточностью, я думаю, что возникла более простая проблема. Возможно, вам стоит 1. Подтвердить, что если ProgressBar.Value действительно не был изменен через событие ProgressBar.ValueChanged. 2. Если результат 1. положительный хотя бы дважды, то подтвердите, что fileTransferStatusEventArgs.Progress менялся каждый раз, а fileTransferStatusEventArgs.Progress имеет значение вашего ожидания. 3. Если результат 1. отрицательный, подтвердите, что доступ к fileTransferStatusEventArgs.Progress вызовет какое-либо исключение.

Alex.Wei 04.05.2018 10:54

Когда Width «отсутствует», проблема, описанная ОП происходит, даже если StatusProgress находится в потоке пользовательского интерфейса (или в противном случае был правильно обработан). И, присваивая Width подходящее значение, как мы предлагали ранее, решает проблему в обоих случаях, для обновлений StatusProgress в потоке пользовательского интерфейса или нет.

jsanalytics 04.05.2018 13:33

Я добавил Width в первый комментарий, который предлагал это, но забыл обновить разметку в вопросе. Разметка вопроса обновлена, а индикатор выполнения по-прежнему не работает.

ProfK 09.05.2018 11:19

В отредактированном вопросе вы блокируете поток пользовательского интерфейса с помощью метода FileDownloader#Download. Можете ли вы показать текущий код загрузчика, который вызывает событие FileTransferStatusChanged? У этого есть такая же проблема?

V.Leon 09.05.2018 14:17
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
19
3 968
6

Ответы 6

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

Вы можете заставить ProgressBar полностью заполнить StatusBarItem, установив StatusBarItemHorizontalContentAlignment на Stretch, или вы можете установить Width на ProgressBar.

Пожалуйста, посмотрите мою правку. ProgressBar отображается без необходимости устанавливать этот HorizontalContentAlignment.

ProfK 02.05.2018 16:23

Да, потому что ваша редакция теперь включает атрибут Width. Я предпочитаю устанавливать HorizontalContentAlignment в Stretch в StatusBarItem вместо статического Width в ProgressBar, потому что теперь у вас есть два места, где определена статическая ширина (GridColumn и ProgressBar).

V.Leon 09.05.2018 14:19

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

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

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

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

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

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

изменения:

 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. Теперь я вижу индикатор выполнения заполнение.

Вы не видите никаких изменений, потому что ваш основной поток AKA Поток пользовательского интерфейса занят Sleeping и у него нет времени обновлять свой UI 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

 }

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

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

Решение

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

  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

Другие вопросы по теме