WPF ProgressBar не обновляется с помощью INotifyPropertyChanged

У меня есть UserControl с несколькими элементами управления TextBox и ProgressBar. Элементы управления TextBox правильно отражают свойства в выделенном коде, к которому они привязаны. Однако ProgressBar не реагирует на изменение свойства.

Мой XAML:

<UserControl
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:Controls="clr-namespace:Cmc.Installer.Controls;assembly=Cmc.Installer.Controls" x:Class="Cmc.Installer.Modules.MobileRecruiter.MobileRecruiterModule" 
             mc:Ignorable="d" 
             d:DesignHeight="600" d:DesignWidth="800">
    <Grid HorizontalAlignment="Left" Height="580" Margin="10,10,0,0" VerticalAlignment="Top" Width="780">
        <Canvas>
            <Label Content="Database Server" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
            <TextBox Text="{Binding DatabaseServer}" Height="23" Canvas.Left="160" Canvas.Top="12" Width="160"/>
            <Label Content="Database Name" HorizontalAlignment="Left" VerticalAlignment="Top" Canvas.Left="10" Canvas.Top="38"/>
            <TextBox Text="{Binding DatabaseName}" Height="23" Canvas.Left="160" Canvas.Top="40" Width="160"/>
            <Label Content="Database Username" HorizontalAlignment="Left" VerticalAlignment="Top" Canvas.Left="10" Canvas.Top="66"/>
            <TextBox Text="{Binding DatabaseUsername}" Height="23" Canvas.Left="160" Canvas.Top="68" Width="160"/>
            <Label Content="Database Password" HorizontalAlignment="Left" VerticalAlignment="Top" Canvas.Left="10" Canvas.Top="94"/>
            <Controls:BindablePasswordBox Password="{Binding DatabasePassword}" Height="23" Canvas.Left="160" Canvas.Top="96" Width="160"/>
            <ProgressBar Name="ProgressBar" Value="{Binding Progress}" Minimum="0" Maximum="100" Canvas.Left="10" Canvas.Top="164" Width="760" Height="24" />
        </Canvas>
    </Grid>
</UserControl>

И его код (очень сокращенный):

public partial class MobileRecruiterModule : UserControl, INotifyPropertyChanged
{
    private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
    private int _progress;

    public MobileRecruiterModule()
    {
        InitializeComponent();
        DataContext = this;
    }

    public string DatabaseServer { get; set; }
    public string DatabaseName { get; set; }
    public string DatabaseUsername { get; set; }
    public string DatabasePassword { get; set; }

    public int Progress
    {
        get { return _progress; }
        set
        {
            if (value == _progress) return;
            _progress = value;
            OnPropertyChanged("Progress");
            Logger.Trace("Progress.set() = " + _progress);
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged(string name)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
    }

    // This is called by an external class
    public void OnProgressChanged(object sender, ProgressChangedEventArgs args)
    {
        Progress = args.ProgressPercentage;
    }

}

Я знаю, что значение Progress меняется, потому что я вижу это в журналах NLog:

2014-04-17 16:22:54.4068|TRACE|Cmc.Installer.Modules.MobileRecruiter.MobileRecruiterModule|Progress.set() = 28

Я не понимаю, почему ProgressBar не обновляется, когда я запускаю OnPropertyChanged в установщике непосредственно перед вызовом регистрации.


person Mark Richman    schedule 17.04.2014    source источник
comment
Как вы обновляете свой Progress объект?   -  person nemesv    schedule 18.04.2014
comment
Через мой собственный обработчик событий. Я обновил свой пример выше.   -  person Mark Richman    schedule 18.04.2014
comment
Вы пробовали выполнить обновление в потоке диспетчера? Действия БД могут дать pb возможность обновить пользовательский интерфейс.   -  person Gayot Fow    schedule 18.04.2014
comment
@GarryVass Не нужно. WPF достаточно умен, чтобы упорядочить его в поток пользовательского интерфейса.   -  person 123 456 789 0    schedule 18.04.2014
comment
@GarryVass в этом приложении нет БД. Прогресс просто обновляется обратным вызовом WCF. Однако эта часть совершенно неуместна.   -  person Mark Richman    schedule 18.04.2014
comment
попробуйте выполнить привязку к тексту текстового блока и посмотреть, обновляется ли он ... просто чтобы узнать, находится ли он из индикатора выполнения   -  person sexta13    schedule 18.04.2014
comment
в консоли ошибок привязки нет ни чего?   -  person John Gardner    schedule 18.04.2014
comment
TextBlock тоже не обновляется. Просто все время отображается ноль (0): <TextBlock Text="{Binding Progress}" Canvas.Left="10" Canvas.Top="193" Width="760" Height="23"></TextBlock>   -  person Mark Richman    schedule 18.04.2014
comment
@MarkRichman, если UI блокируется, это становится очень актуальным.   -  person Gayot Fow    schedule 18.04.2014
comment
Я считаю, что это может быть вызвано тем, как вы на самом деле используете этот UserControl, и что DataContext не такой, как вы думаете. Попробуйте добавить Logger.Trace(DataContext.ToString()) в свой сеттер и посмотрите, что он вам скажет. Также поможет отображение xaml, использующего usercontrol.   -  person Adi Lester    schedule 18.04.2014
comment
у вас есть исключения в коде делегата? кажется, что вы можете получать исключения между потоками, когда пытаетесь обновить прогресс или запустить событие прогресса?   -  person John Gardner    schedule 21.04.2014


Ответы (3)


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

<UserControl x:Class="ProgressBarBinding.Login"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid HorizontalAlignment="Left" Height="580" Margin="10,10,0,0" VerticalAlignment="Top" Width="780">
        <Canvas>
            <Label Content="Database Server" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
            <TextBox Text="{Binding DatabaseServer}" Height="23" Canvas.Left="160" Canvas.Top="12" Width="160"/>
            <Label Content="Database Name" HorizontalAlignment="Left" VerticalAlignment="Top" Canvas.Left="10" Canvas.Top="38"/>
            <TextBox Text="{Binding DatabaseName}" Height="23" Canvas.Left="160" Canvas.Top="40" Width="160"/>
            <Label Content="Database Username" HorizontalAlignment="Left" VerticalAlignment="Top" Canvas.Left="10" Canvas.Top="66"/>
            <TextBox Text="{Binding DatabaseUsername}" Height="23" Canvas.Left="160" Canvas.Top="68" Width="160"/>
            <Label Content="Database Password" HorizontalAlignment="Left" VerticalAlignment="Top" Canvas.Left="10" Canvas.Top="94"/>
            <ProgressBar Name="ProgressBar" Value="{Binding Progress}" Minimum="0" Maximum="100" Canvas.Left="10" Canvas.Top="164" Width="760" Height="24" />
        </Canvas>
    </Grid>
</UserControl>

Единственное, чего здесь не хватает, - это вашего проприетарного контроля паролей, который не влияет на решение.

Я закодировал этот элемент управления в файл MainWindow.xaml таким образом ...

<Window x:Class="ProgressBarBinding.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:ProgressBarBinding"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <vm:ViewModel x:Key="ViewModel"/>
    </Window.Resources>
    <Grid DataContext="{StaticResource ViewModel}">
        <vm:Login/>
    </Grid>
</Window> 

Обратите внимание, что определение ресурса окна включает ссылку на экземпляр модели представления. Большинство людей настраивают MVVM с внедрением зависимостей, но этот подход хорош для быстрых проб и Indicative Code. Модель представления устанавливается как контекст данных Grid. Ваш элемент управления наследует контекст данных из сетки. Это конец кода xaml. В файле MainWindow.xaml.cs нет кода программной части, кроме вызова InitializeComponent (и там, где создается экземпляр виртуальной машины).

Класс ViewModel выглядит так ...

public class ViewModel : INotifyPropertyChanged
{
    private readonly SynchronizationContext _synchronizationContext = SynchronizationContext.Current;
    public ViewModel()
    {
        DatabaseServer = "AnyServer";
        DatabaseName = "Any name";
        Model m = new Model();
        Task.Run(() => m.DoWork(this));
    }
    public string DatabaseServer { get; set; }
    public string DatabaseName { get; set; }
    public string DatabaseUsername { get; set; }
    public string DatabasePassword { get; set; }
    private int _progress;
    public int Progress
    {
        get { return _progress; }
        set
        {
            if (value == _progress) return;
            _progress = value;
            OnPropertyChanged("Progress");
            Console.WriteLine(@"Progress.set() = " + _progress);
        }
    }
    // This is called by an external class
    public void OnProgressChanged(object sender, ProgressChangedEventArgs args)
    {
        _synchronizationContext.Send(delegate { Progress = args.ProgressPercentage; }, null);
    }
    #region INotifyPropertyChanged Implementation
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string name)
    {
        var handler = Interlocked.CompareExchange(ref PropertyChanged, null, null);
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
    }
    #endregion
}

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

Конструктор виртуальной машины запускает модель в потоке TPL. Модель выглядит так ...

public class Model
{
    public void DoWork(ViewModel vm)
    {
        int progressPercentage = 0;
        for (int i = 0; i < 100000; i++)
        {
            vm.OnProgressChanged(this, new ProgressChangedEventArgs(progressPercentage, null));
            if (i%1000 == 0)
            {
                ++progressPercentage;
            }
        }
    }
}

Итак, собрав все вместе, модель работает в своем собственном потоке, а пользовательский интерфейс обновляется в своем собственном потоке. Все работает как положено.

ProgressBar будет увеличиваться до 100, а пользовательский интерфейс останется отзывчивым, пока модель выполняет свою работу. Этот ответ не объясняет, почему ваш исходный код не работает, но я подозреваю, что это связано с истощением потока пользовательского интерфейса. Об этом свидетельствует полная история журнала, но в пользовательском интерфейсе ничего не меняется. В целом, этот ответ приближается к тому, что другие предложили в своих комментариях, а именно к тому, что подход связывания MVVM может многое предложить.

person Gayot Fow    schedule 18.04.2014

Поскольку вы находитесь в UserControl, вам необходимо явно дать ему имя и использовать тег ElementName при привязке, например:

<UserControl
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:Controls="clr-namespace:Cmc.Installer.Controls;assembly=Cmc.Installer.Controls" x:Class="Cmc.Installer.Modules.MobileRecruiter.MobileRecruiterModule" 
             mc:Ignorable="d" 
             d:DesignHeight="600" d:DesignWidth="800" x:Name="MyControl">
    <Grid HorizontalAlignment="Left" Height="580" Margin="10,10,0,0" VerticalAlignment="Top" Width="780">
        <Canvas>
            <ProgressBar Name="ProgressBar" Value="{Binding Progress, ElementName=MyControl}" Minimum="0" Maximum="100" Canvas.Left="10" Canvas.Top="164" Width="760" Height="24" />
        </Canvas>
    </Grid>
</UserControl>
person Yuval Itzchakov    schedule 17.04.2014
comment
К сожалению, это не помогло, и мне пришлось удалить кавычки вокруг ElementName="MyControl", чтобы заставить его скомпилировать: Value="{Binding Progress, ElementName=MyControl}" - person Mark Richman; 18.04.2014
comment
Вы удостоверились, что установили тег x: Name внутри объявления UserControl? - person Yuval Itzchakov; 18.04.2014
comment
Я действительно ... не могли бы вы объяснить, почему это требуется для UserControl? Я новичок в WPF, поэтому все еще думаю о Winforms. - person Mark Richman; 18.04.2014
comment
и OP выполняет DataContext = this;, поэтому привязка к себе должна работать без использования elementname - person John Gardner; 21.04.2014

Зачем вам Binding, если у вас есть Actual control. Поскольку вы не делаете этого в MVVM, просто сразу же вызовите ProgressBar.

ProgressBar.Dispatcher.Invoke(() => ProgressBar.Value = Progress = args.ProgressPercentage);

Извините, но я не вижу преимуществ Binding, если все свойства / элементы управления доступны в вашем классе View.

Связывание будет более полезным и мощным, если вы реализовали MVVM.

person 123 456 789 0    schedule 17.04.2014
comment
Я планирую реализовать MVVM, но это приложение является прототипом. Я просто пытаюсь понять, почему привязка не работает. - person Mark Richman; 18.04.2014
comment
а остальная часть тоже использует привязку, почему прогресс по-другому? - person John Gardner; 18.04.2014
comment
@MarkRichman Что вы имеете в виду под внешним классом? Это работает в другом потоке? И вы уверены, что на самом деле это не обновление индикатора выполнения? Он застревает на значении 0 и просто закрывается? - person 123 456 789 0; 18.04.2014
comment
Значение быстро увеличивается от 0 до 100, о чем свидетельствуют мой журнал и точка останова. У меня есть тысячи записей журнала, показывающих, что вызывается установщик Progress, т.е. 2014-04-17 17:10:07.7871|TRACE|Cmc.Installer.Modules.MobileRecruiter.MobileRecruiterModule|Progress.set() = 44 - person Mark Richman; 18.04.2014
comment
@JohnGardner Потому что, если он будет реализован правильно, он не столкнется с такими странными сценариями, как этот. Использование концепции, которая не должна использоваться в подобных случаях, просто неправильно. - person 123 456 789 0; 18.04.2014
comment
Что именно не так? У меня есть свойство, текст и привязка. Это должно работать. - person Mark Richman; 18.04.2014
comment
это как раз то, для чего нужна привязка данных, привязка значений к элементам управления. здесь просто чего-то не хватает, и все должно работать. ОП пришел сюда не для обсуждения дизайна, он пришел за помощью с конкретным делом. - person John Gardner; 18.04.2014
comment
@MarkRichman Но нет ViewModel. Можете ли вы показать, кто называет это OnProgressChanged или кто поднимает это событие? - person 123 456 789 0; 18.04.2014
comment
У меня есть функция обратного вызова WCF, которая вызывает ее через ссылку на объект через его интерфейс _installModule: public void OnUpdateProgress(int progressPercentage) { Logger.Trace("OnUpdateProgress = " + progressPercentage); var args = new ProgressChangedEventArgs(progressPercentage, null); Progress = progressPercentage; _installModule.OnProgressChanged(this, args); } - person Mark Richman; 18.04.2014
comment
@lll У вас наихудший способ отправки @ ответов;) Как бы здесь выглядела ViewModel? - person Mark Richman; 18.04.2014
comment
@MarkRichman Мне кажется, что есть что-то, что мешает пользовательскому интерфейсу, что у него еще нет времени обновить ProgressBar и просто обновляет его, когда он 100. - person 123 456 789 0; 18.04.2014
comment
@JohnGardner Вы захотите использовать привязку данных для подключения объектной модели (ViewModel) к элементам управления пользовательского интерфейса, а не к свойствам представления пользовательского интерфейса. - person 123 456 789 0; 18.04.2014