Binden einer WPF-ComboBox an eine benutzerdefinierte Liste

183

Ich habe eine ComboBox, die das SelectedItem / SelectedValue nicht zu aktualisieren scheint.

Die ComboBox ItemsSource ist an eine Eigenschaft in einer ViewModel-Klasse gebunden, in der eine Reihe von RAS-Telefonbucheinträgen als CollectionView aufgeführt sind. Dann habe ich (zu unterschiedlichen Zeiten) sowohl die SelectedItemoder SelectedValueeine andere Eigenschaft des ViewModel gebunden . Ich habe dem Befehl save eine MessageBox hinzugefügt, um die von der Datenbindung festgelegten Werte zu debuggen, aber die SelectedItem/ SelectedValue-Bindung wird nicht festgelegt.

Die ViewModel-Klasse sieht ungefähr so ​​aus:

public ConnectionViewModel
{
    private readonly CollectionView _phonebookEntries;
    private string _phonebookeEntry;

    public CollectionView PhonebookEntries
    {
        get { return _phonebookEntries; }
    }

    public string PhonebookEntry
    {
        get { return _phonebookEntry; }
        set
        {
            if (_phonebookEntry == value) return;
            _phonebookEntry = value;
            OnPropertyChanged("PhonebookEntry");
        }
    }
}

Die _phonebookEntries-Auflistung wird im Konstruktor von einem Geschäftsobjekt aus initialisiert. Die ComboBox XAML sieht ungefähr so ​​aus:

<ComboBox ItemsSource="{Binding Path=PhonebookEntries}"
    DisplayMemberPath="Name"
    SelectedValuePath="Name"
    SelectedValue="{Binding Path=PhonebookEntry}" />

Ich interessiere mich nur für den tatsächlichen Zeichenfolgenwert, der in der ComboBox angezeigt wird, nicht für andere Eigenschaften des Objekts, da dies der Wert ist, den ich an RAS übergeben muss, wenn ich die VPN-Verbindung herstellen möchte, DisplayMemberPathund daher SelectedValuePathbeide die Eigenschaft Name von das ConnectionViewModel. Die ComboBox wird in einem Fenster DataTemplateangewendet, dessen DataContext ItemsControlauf eine ViewModel-Instanz festgelegt wurde.

Die ComboBox zeigt die Liste der Elemente korrekt an, und ich kann problemlos eines in der Benutzeroberfläche auswählen. Wenn ich jedoch das Meldungsfeld über den Befehl anzeige, enthält die PhonebookEntry-Eigenschaft weiterhin den Anfangswert und nicht den in der ComboBox ausgewählten Wert. Andere TextBox-Instanzen werden ordnungsgemäß aktualisiert und in der MessageBox angezeigt.

Was fehlt mir bei der Datenbindung der ComboBox? Ich habe viel gesucht und kann anscheinend nichts finden, was ich falsch mache.


Dies ist das Verhalten, das ich sehe, aber es funktioniert aus irgendeinem Grund in meinem speziellen Kontext nicht.

Ich habe ein MainWindowViewModel mit einem CollectionViewConnectionViewModel. In der Code-Behind-Datei MainWindowView.xaml habe ich den DataContext auf das MainWindowViewModel gesetzt. Die Datei MainWindowView.xaml ist ItemsControlan die Sammlung von ConnectionViewModels gebunden. Ich habe eine DataTemplate, die die ComboBox sowie einige andere TextBoxen enthält. Die TextBoxen werden mit direkt an Eigenschaften des ConnectionViewModel gebunden Text="{Binding Path=ConnectionName}".

public class ConnectionViewModel : ViewModelBase
{
    public string Name { get; set; }
    public string Password { get; set; }
}

public class MainWindowViewModel : ViewModelBase
{
    // List<ConnectionViewModel>...
    public CollectionView Connections { get; set; }
}

Der XAML-Code-Behind:

public partial class Window1
{
    public Window1()
    {
        InitializeComponent();
        DataContext = new MainWindowViewModel();
    }
}

Dann XAML:

<DataTemplate x:Key="listTemplate">
    <Grid>
        <ComboBox ItemsSource="{Binding Path=PhonebookEntries}"
            DisplayMemberPath="Name"
            SelectedValuePath="Name"
            SelectedValue="{Binding Path=PhonebookEntry}" />
        <TextBox Text="{Binding Path=Password}" />
    </Grid>
</DataTemplate>

<ItemsControl ItemsSource="{Binding Path=Connections}"
    ItemTemplate="{StaticResource listTemplate}" />

Die TextBoxen werden alle korrekt gebunden, und die Daten werden problemlos zwischen ihnen und dem ViewModel verschoben. Es ist nur die ComboBox, die nicht funktioniert.

Ihre Annahme bezüglich der PhonebookEntry-Klasse ist richtig.

Ich gehe davon aus, dass der von meiner DataTemplate verwendete DataContext automatisch über die Bindungshierarchie festgelegt wird, sodass ich ihn nicht für jedes Element in der explizit festlegen muss ItemsControl. Das kommt mir ein bisschen albern vor.


Hier ist eine Testimplementierung, die das Problem anhand des obigen Beispiels demonstriert.

XAML:

<Window x:Class="WpfApplication7.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">
    <Window.Resources>
        <DataTemplate x:Key="itemTemplate">
            <StackPanel Orientation="Horizontal">
                <TextBox Text="{Binding Path=Name}" Width="50" />
                <ComboBox ItemsSource="{Binding Path=PhonebookEntries}"
                    DisplayMemberPath="Name"
                    SelectedValuePath="Name"
                    SelectedValue="{Binding Path=PhonebookEntry}"
                    Width="200"/>
            </StackPanel>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <ItemsControl ItemsSource="{Binding Path=Connections}"
            ItemTemplate="{StaticResource itemTemplate}" />
    </Grid>
</Window>

Der Code-Behind :

namespace WpfApplication7
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
            DataContext = new MainWindowViewModel();
        }
    }

    public class PhoneBookEntry
    {
        public string Name { get; set; }
        public PhoneBookEntry(string name)
        {
            Name = name;
        }
    }

    public class ConnectionViewModel : INotifyPropertyChanged
    {

        private string _name;

        public ConnectionViewModel(string name)
        {
            _name = name;
            IList<PhoneBookEntry> list = new List<PhoneBookEntry>
                                             {
                                                 new PhoneBookEntry("test"),
                                                 new PhoneBookEntry("test2")
                                             };
            _phonebookEntries = new CollectionView(list);
        }
        private readonly CollectionView _phonebookEntries;
        private string _phonebookEntry;

        public CollectionView PhonebookEntries
        {
            get { return _phonebookEntries; }
        }

        public string PhonebookEntry
        {
            get { return _phonebookEntry; }
            set
            {
                if (_phonebookEntry == value) return;
                _phonebookEntry = value;
                OnPropertyChanged("PhonebookEntry");
            }
        }

        public string Name
        {
            get { return _name; }
            set
            {
                if (_name == value) return;
                _name = value;
                OnPropertyChanged("Name");
            }
        }
        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        public event PropertyChangedEventHandler PropertyChanged;
    }

    public class MainWindowViewModel
    {
        private readonly CollectionView _connections;

        public MainWindowViewModel()
        {
            IList<ConnectionViewModel> connections = new List<ConnectionViewModel>
                                                          {
                                                              new ConnectionViewModel("First"),
                                                              new ConnectionViewModel("Second"),
                                                              new ConnectionViewModel("Third")
                                                          };
            _connections = new CollectionView(connections);
        }

        public CollectionView Connections
        {
            get { return _connections; }
        }
    }
}

Wenn Sie dieses Beispiel ausführen, erhalten Sie das Verhalten, über das ich spreche. Die TextBox aktualisiert ihre Bindung in Ordnung, wenn Sie sie bearbeiten, die ComboBox jedoch nicht. Sehr verwirrend, da ich wirklich nur ein übergeordnetes ViewModel vorgestellt habe.

Ich arbeite derzeit unter dem Eindruck, dass ein Element, das an das untergeordnete Element eines DataContext gebunden ist, dieses untergeordnete Element als DataContext hat. Ich kann keine Dokumentation finden, die dies auf die eine oder andere Weise klärt.

Dh

Fenster -> DataContext = MainWindowViewModel
..Items -> An DataContext.PhonebookEntries gebunden
.... Item -> DataContext = PhonebookEntry (implizit zugeordnet)

Ich weiß nicht, ob das meine Annahme besser erklärt (?).


Um meine Annahme zu bestätigen, ändern Sie die Bindung der TextBox in

<TextBox Text="{Binding Mode=OneWay}" Width="50" />

Dies zeigt, dass der TextBox-Bindungsstamm (den ich mit dem DataContext vergleiche) die ConnectionViewModel-Instanz ist.

Geoff Bennett
quelle

Antworten:

189

Sie setzen DisplayMemberPath und SelectedValuePath auf "Name", daher gehe ich davon aus, dass Sie eine Klasse PhoneBookEntry mit einer öffentlichen Eigenschaft Name haben.

Haben Sie den DataContext auf Ihr ConnectionViewModel-Objekt festgelegt?

Ich habe Ihren Code kopiert und einige kleinere Änderungen vorgenommen, und es scheint gut zu funktionieren. Ich kann die PhoneBookEnty-Eigenschaft für Ansichtsmodelle festlegen und das ausgewählte Element in der Combobox ändern. Außerdem kann ich das ausgewählte Element in der Combobox ändern und die PhoneBookEntry-Eigenschaft für Ansichtsmodelle wird korrekt festgelegt.

Hier ist mein XAML-Inhalt:

<Window x:Class="WpfApplication6.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="300" Width="300">
<Grid>
    <StackPanel>
        <Button Click="Button_Click">asdf</Button>
        <ComboBox ItemsSource="{Binding Path=PhonebookEntries}"
                  DisplayMemberPath="Name"
                  SelectedValuePath="Name"
                  SelectedValue="{Binding Path=PhonebookEntry}" />
    </StackPanel>
</Grid>
</Window>

Und hier ist mein Code-Behind:

namespace WpfApplication6
{

    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
            ConnectionViewModel vm = new ConnectionViewModel();
            DataContext = vm;
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            ((ConnectionViewModel)DataContext).PhonebookEntry = "test";
        }
    }

    public class PhoneBookEntry
    {
        public string Name { get; set; }

        public PhoneBookEntry(string name)
        {
            Name = name;
        }

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

    public class ConnectionViewModel : INotifyPropertyChanged
    {
        public ConnectionViewModel()
        {
            IList<PhoneBookEntry> list = new List<PhoneBookEntry>();
            list.Add(new PhoneBookEntry("test"));
            list.Add(new PhoneBookEntry("test2"));
            _phonebookEntries = new CollectionView(list);
        }

        private readonly CollectionView _phonebookEntries;
        private string _phonebookEntry;

        public CollectionView PhonebookEntries
        {
            get { return _phonebookEntries; }
        }

        public string PhonebookEntry
        {
            get { return _phonebookEntry; }
            set
            {
                if (_phonebookEntry == value) return;
                _phonebookEntry = value;
                OnPropertyChanged("PhonebookEntry");
            }
        }

        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        public event PropertyChangedEventHandler PropertyChanged;
    }
}

Edit: Geoffs zweites Beispiel scheint nicht zu funktionieren, was mir etwas seltsam erscheint. Wenn ich die PhonebookEntries-Eigenschaft im ConnectionViewModel vom Typ ReadOnlyCollection ändere, funktioniert die TwoWay-Bindung der SelectedValue-Eigenschaft in der Combobox einwandfrei.

Möglicherweise liegt ein Problem mit der CollectionView vor? Ich habe eine Warnung in der Ausgabekonsole bemerkt:

System.Windows.Data Warnung: 50: Die direkte Verwendung von CollectionView wird nicht vollständig unterstützt. Die Grundfunktionen funktionieren zwar mit einigen Ineffizienzen, bei erweiterten Funktionen können jedoch bekannte Fehler auftreten. Verwenden Sie eine abgeleitete Klasse, um diese Probleme zu vermeiden.

Edit2 (.NET 4.5): Der Inhalt der DropDownList kann auf ToString () und nicht auf DisplayMemberPath basieren, während DisplayMemberPath das Mitglied nur für das ausgewählte und angezeigte Element angibt.

Kjetil Watnedal
quelle
1
Ich habe bemerkt , dass Nachricht als gut, aber ich nahm an, was war bedeckt habe verbindlich Grunddaten gewesen. Ich denke nicht. :) Ich mache jetzt die Eigenschaften als IList <T >und im Property Getter mit _list.AsReadOnly () verfügbar, ähnlich wie Sie es erwähnt haben. Es funktioniert so, wie ich es mir von der ursprünglichen Methode erhofft hätte. Außerdem fiel mir ein, dass ich, während die ItemsSource-Bindung einwandfrei funktionierte, einfach die Current-Eigenschaft im ViewModel hätte verwenden können, um auf das ausgewählte Element in der ComboBox zuzugreifen. Es fühlt sich jedoch nicht so natürlich an wie das Binden der ComboBoxes SelectedValue / SelectedItem-Eigenschaft.
Geoff Bennett
3
Ich kann bestätigen, dass das Ändern der Sammlung, an die die ItemsSourceEigenschaft gebunden ist, in eine schreibgeschützte Sammlung funktioniert. In meinem Fall musste ich es von ObservableCollectionauf ändern ReadOnlyObservableCollection. Nüsse. Dies ist .NET 3.5 - nicht sicher, ob es in 4.0
behoben
74

So binden Sie die Daten an ComboBox

List<ComboData> ListData = new List<ComboData>();
ListData.Add(new ComboData { Id = "1", Value = "One" });
ListData.Add(new ComboData { Id = "2", Value = "Two" });
ListData.Add(new ComboData { Id = "3", Value = "Three" });
ListData.Add(new ComboData { Id = "4", Value = "Four" });
ListData.Add(new ComboData { Id = "5", Value = "Five" });

cbotest.ItemsSource = ListData;
cbotest.DisplayMemberPath = "Value";
cbotest.SelectedValuePath = "Id";

cbotest.SelectedValue = "2";

ComboData sieht aus wie:

public class ComboData
{ 
  public int Id { get; set; } 
  public string Value { get; set; } 
}
Roy
quelle
Diese Lösung funktioniert bei mir nicht. Die ItemsSource funktioniert einwandfrei, aber die Path-Eigenschaften werden nicht korrekt auf ComboData-Werte umgeleitet.
Coneone
3
Idund Valuemüssen Eigenschaften sein , kein Klassenfeld, wie:public class ComboData { public int Id { get; set; } public string Value { get; set; } }
Edgar
23

Ich hatte ein zunächst identisches Problem, das sich jedoch aufgrund eines NHibernate / WPF-Kompatibilitätsproblems herausstellte. Das Problem wurde durch die Art und Weise verursacht, wie WPF die Objektgleichheit überprüft. Ich konnte meine Inhalte mithilfe der Objekt-ID-Eigenschaft in den Eigenschaften SelectedValue und SelectedValuePath zum Laufen bringen.

<ComboBox Name="CategoryList"
          DisplayMemberPath="CategoryName"
          SelectedItem="{Binding Path=CategoryParent}"
          SelectedValue="{Binding Path=CategoryParent.ID}"
          SelectedValuePath="ID">

Weitere Informationen finden Sie im Blogbeitrag von Chester, The WPF ComboBox - SelectedItem, SelectedValue und SelectedValuePath mit NHibernate .

CyberMonk
quelle
1

Ich hatte ein ähnliches Problem, bei dem das SelectedItem nie aktualisiert wurde.

Mein Problem war, dass das ausgewählte Element nicht dieselbe Instanz war wie das in der Liste enthaltene Element. Also musste ich einfach die Equals () -Methode in meinem MyCustomObject überschreiben und die IDs dieser beiden Instanzen vergleichen, um der ComboBox mitzuteilen, dass es sich um dasselbe Objekt handelt.

public override bool Equals(object obj)
{
    return this.Id == (obj as MyCustomObject).Id;
}
Phifi
quelle