2 Stimmen

Verdrahtung von CollectionChanged und PropertyChanged (Oder: Warum aktualisieren einige WPF-Bindungen nicht?)

WPF DataBindings verwendet, um mich glücklich zu machen. Eine Sache, die ich gerade jetzt gestolpert ist, dass an einem gewissen Punkt sie einfach nicht wie beabsichtigt aktualisieren. Bitte werfen Sie einen Blick auf die folgenden (ziemlich einfach) Code:

<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>

Der Code hinter der 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;
    }
  }
}

Zum Schluss noch die ViewModel-Klassen:

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;
}

Was ich mir wünschen würde: Wenn ich einen Eintrag in der ListBox auswähle und seinen Namen in der TextBox ändere, wird die Liste aktualisiert, um den neuen Wert anzuzeigen.

Was passiert: nichts. Und das ist das richtige Verhalten, wenn ich irgendein Richter bin. Ich habe dafür gesorgt, dass die PropertyChanged des SelectedItem ausgelöst wird, aber das führt (natürlich) nicht dazu, dass CollectionChanged ausgelöst wird.

Um dies zu beheben, habe ich eine von ObservableCollection abgeleitete Klasse erstellt, die eine öffentliche OnCollectionChanged-Methode hat, siehe hier:

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

Ich greife darauf über den Konstruktor des ViewModel zu, wie unten beschrieben:

    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();
    }

Es funktioniert, aber es ist keine saubere Lösung. Meine nächste Idee wäre, ein Derivat von ObservableCollection zu erstellen, das die Verdrahtung automatisch in einem CollectionChanged-Handler vornimmt.

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));
    }
}

Die Frage ist: Gibt es einen besseren Weg, dies zu tun? Ist dies wirklich notwendig?

Vielen Dank im Voraus für jeden Beitrag!

4voto

Dan Puzey Punkte 32863

Nein, das ist überhaupt nicht notwendig. Der Grund für das Scheitern Ihrer Probe ist subtil, aber ganz einfach.

Wenn Sie WPF keine Vorlage für ein Datenelement zur Verfügung stellen (wie z. B. die Person Objekte in Ihrer Liste), wird standardmäßig die ToString() Methode anzuzeigen. Das ist ein Mitglied, keine Eigenschaft, und daher erhalten Sie keine Ereignisbenachrichtigung, wenn sich der Wert ändert.

Wenn Sie Folgendes hinzufügen DisplayMemberPath="Name" zu Ihrer Listbox hinzufügen, wird eine Vorlage generiert, die korrekt mit der Name Ihrer Person - die dann automatisch aktualisiert wird, wie Sie es erwarten würden.

1voto

devdigital Punkte 33882

Ich glaube, dies ist mit Ihrer ToString() Überschreibung auf PersonViewModel zu tun. Wenn Sie dies entfernen und stattdessen ein DataTemplate für die ListBox verwenden, sollten Sie das erwartete Verhalten erhalten:

<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>

1voto

decyclone Punkte 29526

Hinzufügen. DisplayMemberPath="Name" a ListBox . Das Problem ist, dass Sie sich auf Folgendes verlassen ToString() um den Namen der Person und keine Eigenschaft anzuzeigen. Deshalb ist das Anheben PropertyChanged macht keinen Unterschied. Verwenden Sie von nun an keine Methode mehr, um einen Wert in Bindings auszuwerten.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X