Подключение CollectionChanged и PropertyChanged (или: Почему некоторые привязки WPF не обновляются?) - PullRequest
2 голосов
/ 08 декабря 2010

WPF DataBindings раньше делали меня счастливым.Одна вещь, на которую я наткнулся только сейчас, - это то, что в какой-то момент они просто не обновляются, как задумано.Пожалуйста, взгляните на следующий (довольно простой) код:

<Window x:Class="CVFix.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="300">
  <Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition></RowDefinition>
        <RowDefinition Height="40"></RowDefinition>
    </Grid.RowDefinitions>
    <ListBox Grid.Row="0" Grid.Column="0" 
                  ItemsSource="{Binding Path=Persons}"
                  SelectedItem="{Binding Path=SelectedPerson}"
                  x:Name="lbPersons"></ListBox>
    <TextBox Grid.Row="1" Grid.Column="0" Text="{Binding Path=SelectedPerson.Name, UpdateSourceTrigger=PropertyChanged}"/>
  </Grid>
</Window>

Код XAML:

using System.Windows;
namespace CVFix
{
  /// <summary>
  /// Interaction logic for MainWindow.xaml
  /// </summary>
  public partial class MainWindow : Window
  {
    public ViewModel Model { get; set; }

    public MainWindow()
    {
        InitializeComponent();
        this.Model = new ViewModel();
        this.DataContext = this.Model;
    }
  }
}

Наконец, вот классы ViewModel:

using System.Collections.ObjectModel;
using System.ComponentModel;

namespace CVFix
{
  public class ViewModel : INotifyPropertyChanged
  {
    private PersonViewModel selectedPerson;

    public PersonViewModel SelectedPerson
    {
        get { return this.selectedPerson; }
        set
        {
            this.selectedPerson = value;

            if (this.PropertyChanged != null)
                this.PropertyChanged(this, new PropertyChangedEventArgs("SelectedPerson"));
        }
    }

    public ObservableCollection<PersonViewModel> Persons { get; set; }

    public ViewModel()
    {
        this.Persons = new ObservableCollection<PersonViewModel>();
        this.Persons.Add(new PersonViewModel() { Name = "Adam" });
        this.Persons.Add(new PersonViewModel() { Name = "Bobby" });
        this.Persons.Add(new PersonViewModel() { Name = "Charles" });
    }

    public event PropertyChangedEventHandler PropertyChanged;
  }
}

public class PersonViewModel : INotifyPropertyChanged
{
    private string name;

    public string Name
    {
        get { return this.name; }
        set
        {
            this.name = value;
            if(this.PropertyChanged != null)
            this.PropertyChanged(this, new PropertyChangedEventArgs("Name"));
        }
    }

    public override string ToString()
    {
        return this.Name;
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

Что бы я хотел, чтобы произошло: когда я выбираю запись в ListBox и изменяю ее имя в TextBox, список обновляется для отображения нового значения.

Что происходит: ничего.И это правильное поведение, если я любой судья.Я убедился, что PropertyChanged объекта SelectedItem запущен, но это (конечно) не приводит к запуску CollectionChanged.

Чтобы исправить это, я создал производный от ObservableCollection класс, который имеет открытый метод OnCollectionChanged, см. Здесь:

public class PersonList : ObservableCollection<PersonViewModel>
{
    public void OnCollectionChanged()
    {
        this.OnCollectionChanged(new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Reset ));
    }
}

Я получаю доступ к этому из конструктора ViewModel, как описано ниже:

    public ViewModel()
    {
        PersonViewModel vm1 = new PersonViewModel()
        {
            Name = "Adam"
        };
        PersonViewModel vm2 = new PersonViewModel()
        {
            Name = "Bobby"
        };
        PersonViewModel vm3 = new PersonViewModel()
        {
            Name = "Charles"
        };
        vm1.PropertyChanged += this.PersonChanged;

        this.Persons = new PersonList();


        this.Persons.Add(vm1);
        this.Persons.Add(vm2);
        this.Persons.Add(vm3);
    }

    void PersonChanged(object sender, PropertyChangedEventArgs e)
    {
        this.Persons.OnCollectionChanged();
    }

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

public class SynchronizedObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                {
                    foreach (INotifyPropertyChanged item in e.NewItems)
                    {
                        item.PropertyChanged += this.ItemChanged;
                    }
                    break;
                }

            case NotifyCollectionChangedAction.Remove:
                {
                    foreach (INotifyPropertyChanged item in e.OldItems)
                    {
                        item.PropertyChanged -= this.ItemChanged;
                    }
                    break;
                }
        }
        base.OnCollectionChanged(e);
    }

    void ItemChanged(object sender, PropertyChangedEventArgs e)
    {
        this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
}

Вопрос: есть ли лучший способ сделать это?Это действительно необходимо?

Заранее большое спасибо за любой вклад!

Ответы [ 3 ]

4 голосов
/ 08 декабря 2010

Нет, это совсем не обязательно. Причина, по которой ваш образец терпит неудачу, неуловима, но довольно проста.

Если вы не предоставите WPF шаблон для элемента данных (например, объектов Person в вашем списке), по умолчанию будет использоваться метод ToString() для отображения. Это элемент, а не свойство, поэтому при изменении значения вы не получите уведомление о событии.

Если вы добавите DisplayMemberPath="Name" в свой список, он сгенерирует шаблон, который будет правильно привязан к Name вашего лица - который затем автоматически обновится, как вы ожидаете.

1 голос
/ 08 декабря 2010

добавить DisplayMemberPath="Name" к ListBox. Проблема в том, что вы используете ToString() для отображения имени человека, а не его свойства Вот почему повышение PropertyChanged не имеет никакого значения. Отныне не используйте метод для оценки каких-либо значений в привязках.

1 голос
/ 08 декабря 2010

Я считаю, что это связано с переопределением ToString () в PersonViewModel.Если вы удалите это и вместо этого будете использовать DataTemplate в ListBox, вы должны получить ожидаемое поведение:

<Window x:Class="CVFix.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="300">
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition></RowDefinition>
        <RowDefinition Height="40"></RowDefinition>
    </Grid.RowDefinitions>
    <ListBox Grid.Row="0" Grid.Column="0" 
              ItemsSource="{Binding Path=Persons}"
              SelectedItem="{Binding Path=SelectedPerson}"
              x:Name="lbPersons">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Name}" />
            </DataTemplate>
        </ListBox.ItemTemplate>            
    </ListBox>
    <TextBox Grid.Row="1" Grid.Column="0" Text="{Binding Path=SelectedPerson.Name, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>

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