Это случайный и прототипный код, поэтому я пробую то, что, по моему мнению, должно работать, гуглил, если это не так, а затем спрашивал здесь после просмотра похожих вопросов.
В моем представлении 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
, установив StatusBarItem
HorizontalContentAlignment
на 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
?