Это случайный и прототипный код, поэтому я пробую то, что, по моему мнению, должно работать, гуглил, если это не так, а затем спрашивал здесь после просмотра похожих вопросов.
В моем представлении 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 не завершит цикл, а затем внезапно индикатор выполнения показывает полный прогресс, то есть завершение.
Что за разметка для строки состояния? В нем должна быть сетка, но какие столбцы в ней есть? Если вы временно поместите прямоугольник с красной заливкой в Grid.Column = "2", это будет видно. Возможно, вам нужно установить ширину панели прогресса или столбца, в котором она находится.
Я бы внимательно посмотрел на проблемы с потоками, даже если статус сообщения действительно отображается. Например, событие может быть сначала вызвано в потоке пользовательского интерфейса (т.е. вызывается event.Invoke ("myfile.txt", 0)). Последующие вызовы могут быть из потока, не относящегося к пользовательскому интерфейсу. Когда поток, не связанный с пользовательским интерфейсом, вызывает это событие, SetProperty сначала устанавливает ваше личное поле. Итак, в ViewModel поле обновляется. Тогда будущие чтения свойств покажут ожидаемое значение. Но SetProperty молча терпит неудачу, когда вызывает привязку, которая является однопоточной.
Кстати, я бы порекомендовал посмотреть на await / async и другие инструменты многопоточности. Задача Task.Run может быть сложной для гарантии того, что обратные вызовы происходят в незаблокированном потоке пользовательского интерфейса. С помощью await / async все приложение может концептуально быть однопоточным, при этом единственный поток запускается с того места, где он оставался после завершения ожидания.
Mode = TwoWay на индикаторе выполнения? Незачем. Ничего не пахнет, поэтому я бы рекомендовал зацепиться за Snoop, чтобы проверить ваши привязки во время выполнения.
@mwwaters Я использую async, но не другие инструменты для работы с потоками. Это потому, что вся моя инфраструктура построена на использовании HttpClient, который имеет только методы async comms.
@Andy В строке состояния есть сетка, и даже когда я установил ширину столбца для индикатора выполнения, все, что у меня было, это серый прямоугольник шириной 300, но с тонкой зеленой линией в самом левом углу.
@ Буду ли я всегда - я не так часто делаю XAML - забываю, в каком направлении находится привязка по умолчанию, поэтому я просто ударил Mode=TwoWay как одну из многих вещей, которые я пытался заставить работать индикатор выполнения. Я посмотрю со Снупом, спасибо.
Свойства INPC можно изменить из любого потока. Класс Binding будет маршалировать обновление за вас в поток пользовательского интерфейса. Я действительно не вижу здесь ничего плохого. Если вы видите, что число меняется, это говорит о том, что вы не наказываете пользовательский интерфейс - частую причину этой проблемы. Может быть, fileTransferStatusEventArgs.Progress всегда равен 0? Я бы прошел через весь процесс, убедился, что он не равен 0, проверил, что свойство читается правильно, затем использовал Snoop для проверки состояния индикатора выполнения, значения inc, min / max, ширины и высоты, макета и т. д. Сделайте 0 предположений, потому что происходит что-то странное.
Я не могу воспроизвести проблему. Вы уверены, что все настроено правильно, т.е. StatusProgress принимает значения в диапазоне от 0 до 100, а не от 0 до 1 (это объясняет тонкую зеленую линию слева)?
предоставьте XAML, окружающий приведенный выше образец, чтобы помочь
Укажите ширину индикатора выполнения: <ProgressBar Value = "{Binding StatusProgress, Mode=TwoWay}" Height = "16" Width = "100"/>.
Нет Widthвоспроизводит вашу проблему.
С Widthрешает вашу проблему.
Мне кажется, он чист. Я не могу понять это, но если кажется, что StatusProgress обновляется должным образом, то мне придется винить уведомление пользовательского интерфейса. Попробуйте добавить (да, избыточный и урезанный (в зависимости от фреймворка)) вызов к OnPropertyChanged(). Если он ничего не меняет, значит, вы исключили почти все, кроме проблемы с потоками.
Поскольку DataBindEngine отлично работает с многопоточностью, я думаю, что возникла более простая проблема. Возможно, вам стоит 1. Подтвердить, что если ProgressBar.Value действительно не был изменен через событие ProgressBar.ValueChanged. 2. Если результат 1. положительный хотя бы дважды, то подтвердите, что fileTransferStatusEventArgs.Progress менялся каждый раз, а fileTransferStatusEventArgs.Progress имеет значение вашего ожидания. 3. Если результат 1. отрицательный, подтвердите, что доступ к fileTransferStatusEventArgs.Progress вызовет какое-либо исключение.
Когда Width «отсутствует», проблема, описанная ОП происходит, даже если StatusProgress находится в потоке пользовательского интерфейса (или в противном случае был правильно обработан). И, присваивая Width подходящее значение, как мы предлагали ранее, решает проблему в обоих случаях, для обновлений StatusProgress в потоке пользовательского интерфейса или нет.
Я добавил Width в первый комментарий, который предлагал это, но забыл обновить разметку в вопросе. Разметка вопроса обновлена, а индикатор выполнения по-прежнему не работает.
В отредактированном вопросе вы блокируете поток пользовательского интерфейса с помощью метода FileDownloader#Download. Можете ли вы показать текущий код загрузчика, который вызывает событие FileTransferStatusChanged? У этого есть такая же проблема?





Это происходит потому, что стиль StatusBarItem по умолчанию устанавливает для своего HorizontalContentAlignment значение Left, что приводит к тому, что ProgressBar занимает лишь небольшое пространство по горизонтали.
Вы можете заставить ProgressBar полностью заполнить StatusBarItem, установив StatusBarItemHorizontalContentAlignment на Stretch, или вы можете установить Width на ProgressBar.
Пожалуйста, посмотрите мою правку. ProgressBar отображается без необходимости устанавливать этот HorizontalContentAlignment.
Да, потому что ваша редакция теперь включает атрибут Width. Я предпочитаю устанавливать HorizontalContentAlignment в Stretch в StatusBarItem вместо статического Width в ProgressBar, потому что теперь у вас есть два места, где определена статическая ширина (GridColumn и ProgressBar).
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

Позвольте Task выполнять вашу длительную работу и основной поток для обновления пользовательского интерфейса
Оберните свой код внутри Task, и вы увидите, как прогрессирует индикатор выполнения.
private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
await Task.Run(() =>
{
_viewModel.Download();
});
//_viewModel.Download(); //this will run on UI Thread
}
Все, что не связано с пользовательским интерфейсом, должно выполняться в задачах, потому что в противном случае вы блокируете поток пользовательского интерфейса и пользовательский интерфейс. В вашем случае загрузка происходила в потоке пользовательского интерфейса, последний ожидал завершения загрузки перед обновлением пользовательского интерфейса.
Вам нужно сделать две вещи, чтобы решить вашу проблему:
удалить работу из потока пользовательского интерфейса.
убедитесь, что работа может взаимодействовать с вашим потоком пользовательского интерфейса.
Итак, сначала начните загрузку как 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
Будет ли это выглядеть иначе, если вы установите
HorizontalContentAlignment = "Stretch"наStatusBarItem, содержащийProgressBar?