Mengikat WPF ComboBox ke daftar kustom

183

Saya memiliki ComboBox yang sepertinya tidak memperbarui SelectedItem / SelectedValue.

ComboBox ItemsSource terikat ke properti pada kelas ViewModel yang mencantumkan sekelompok entri buku telepon RAS sebagai CollectionView. Kemudian saya terikat (pada waktu yang terpisah) baik ke SelectedItematau SelectedValueke properti lain dari ViewModel. Saya telah menambahkan MessageBox ke dalam perintah save untuk men-debug nilai yang ditetapkan oleh penyatuan data, tetapi SelectedItem/ SelectedValuebinding tidak diatur.

Kelas ViewModel terlihat seperti ini:

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

Koleksi _phonebookEntries sedang diinisialisasi dalam konstruktor dari objek bisnis. ComboBox XAML terlihat seperti ini:

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

Saya hanya tertarik pada nilai string aktual yang ditampilkan di ComboBox, bukan properti lain dari objek karena ini adalah nilai yang harus saya sampaikan kepada RAS ketika saya ingin membuat koneksi VPN, karenanya DisplayMemberPathdan SelectedValuePathkeduanya merupakan properti Nama dari ConnectionViewModel. ComboBox DataTemplatediterapkan pada ItemsControlpada Window yang DataContext telah diatur ke instance ViewModel.

ComboBox menampilkan daftar item dengan benar, dan saya dapat memilih satu di UI tanpa masalah. Namun ketika saya menampilkan kotak pesan dari perintah, properti PhonebookEntry masih memiliki nilai awal di dalamnya, bukan nilai yang dipilih dari ComboBox. Contoh TextBox lainnya memperbarui baik dan ditampilkan di MessageBox.

Apa yang saya lewatkan dengan menyatukan ComboBox? Saya telah melakukan banyak pencarian dan sepertinya tidak dapat menemukan kesalahan yang saya lakukan.


Ini adalah perilaku yang saya lihat, tetapi itu tidak berhasil karena alasan tertentu dalam konteks khusus saya.

Saya memiliki MainWindowViewModel yang memiliki CollectionViewConnectionViewModels. Dalam file kode MainWindowView.xaml di belakang, saya mengatur DataContext ke MainWindowViewModel. MainWindowView.xaml memiliki ItemsControlikatan dengan koleksi ConnectionViewModels. Saya memiliki DataTemplate yang menampung ComboBox dan juga beberapa TextBox lainnya. TextBox terikat langsung ke properti ConnectionViewModel menggunakan 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; }
}

Kode XAML di belakang:

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

Kemudian 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}" />

Semua TextBox mengikat dengan benar, dan data bergerak di antara mereka dan ViewModel tanpa masalah. Hanya ComboBox yang tidak berfungsi.

Anda benar dalam asumsi Anda mengenai kelas Buku telepon.

Asumsi yang saya buat adalah bahwa DataContext yang digunakan oleh DataTemplate saya secara otomatis diatur melalui hierarki yang mengikat, sehingga saya tidak perlu mengaturnya secara eksplisit untuk setiap item dalam ItemsControl. Bagi saya itu agak konyol.


Berikut ini adalah implementasi tes yang menunjukkan masalah, berdasarkan contoh di atas.

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>

The kode-belakang :

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

Jika Anda menjalankan contoh itu, Anda akan mendapatkan perilaku yang saya bicarakan. TextBox memperbarui denda pengikatan saat Anda mengeditnya, tetapi ComboBox tidak. Sangat membingungkan melihat sebagai satu-satunya hal yang saya lakukan adalah memperkenalkan ViewModel induk.

Saya saat ini bekerja di bawah kesan bahwa item yang terikat pada anak dari DataContext memiliki anak itu sebagai DataContext-nya. Saya tidak dapat menemukan dokumentasi yang membersihkan ini dengan satu atau lain cara.

Yaitu,

Window -> DataContext = MainWindowViewModel
..Items -> Bound ke DataContext.PhonebookEntries
.... Item -> DataContext = PhonebookEntry (terkait secara implisit)

Saya tidak tahu apakah itu menjelaskan asumsi saya lebih baik (?).


Untuk mengkonfirmasi asumsi saya, ubah pengikatan TextBox menjadi

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

Dan ini akan menunjukkan akar pengikatan TextBox (yang saya bandingkan dengan DataContext) adalah instance ConnectionViewModel.

Geoff Bennett
sumber

Jawaban:

189

Anda mengatur DisplayMemberPath dan SelectedValuePath ke "Nama", jadi saya berasumsi bahwa Anda memiliki PhoneBookEntry kelas dengan Nama properti publik.

Sudahkah Anda mengatur DataContext ke objek ConnectionViewModel Anda?

Saya menyalin kode Anda dan membuat beberapa modifikasi kecil, dan tampaknya berfungsi dengan baik. Saya dapat mengatur viewmodels properti PhoneBookEnty dan item yang dipilih dalam perubahan combobox, dan saya dapat mengubah item yang dipilih di combobox dan model tampilan properti PhoneBookEntry diatur dengan benar.

Inilah konten XAML saya:

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

Dan ini kode saya di belakang:

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

Sunting: Geoffs contoh kedua tampaknya tidak berfungsi, yang tampaknya agak aneh bagi saya. Jika saya mengubah properti PhonebookEntries pada ConnectionViewModel menjadi tipe ReadOnlyCollection , pengikatan TwoWay dari properti SelectedValue pada combobox berfungsi dengan baik.

Mungkin ada masalah dengan CollectionView? Saya perhatikan ada peringatan di konsol keluaran:

Peringatan System.Windows.Data: 50: Menggunakan CollectionView secara langsung tidak sepenuhnya didukung. Fitur dasar berfungsi, meskipun dengan beberapa inefisiensi, tetapi fitur lanjutan mungkin menemui bug yang diketahui. Pertimbangkan menggunakan kelas turunan untuk menghindari masalah ini.

Edit2 (.NET 4.5): Konten DropDownList dapat didasarkan pada ToString () dan bukan dari DisplayMemberPath, sementara DisplayMemberPath menentukan anggota untuk item yang dipilih dan yang ditampilkan saja.

Kjetil Watnedal
sumber
1
Saya memang memperhatikan pesan itu juga, tapi saya berasumsi apa yang dicakup akan menjadi data dasar yang mengikat. Saya rasa tidak. :) Saya sekarang mengekspos properti sebagai IList <T >dan di pengambil properti menggunakan _list.AsReadOnly () mirip dengan cara yang Anda sebutkan. Ini bekerja seperti yang saya harapkan metode asli akan melakukannya. Selain itu, terlintas dalam pikiran saya bahwa sementara pengikatan ItemsSource bekerja dengan baik, saya bisa saja menggunakan properti Current di ViewModel untuk mengakses item yang dipilih di ComboBox. Namun, itu tidak terasa alami seperti mengikat properti ComboBoxes SelectedValue / SelectedItem.
Geoff Bennett
3
Saya dapat mengonfirmasi bahwa mengubah koleksi, yang ItemsSourcemengikat properti, menjadi hanya baca membuatnya berfungsi. Dalam kasus saya, saya harus mengubahnya dari ObservableCollectionmenjadi ReadOnlyObservableCollection. Gila. Ini .NET 3.5 - tidak yakin apakah sudah diperbaiki di 4.0
ChrisWue
74

Untuk mengikat data ke 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 terlihat seperti:

public class ComboData
{ 
  public int Id { get; set; } 
  public string Value { get; set; } 
}
Roy
sumber
Solusi ini tidak berfungsi untuk saya. The ItemsSource berfungsi dengan baik, tetapi properti Path tidak mengarahkan dengan benar ke nilai ComboData.
Coneone
3
Iddan Valueharus properti , bukan bidang kelas, seperti:public class ComboData { public int Id { get; set; } public string Value { get; set; } }
Edgar
23

Saya memiliki apa yang awalnya tampak sebagai masalah yang identik, tetapi ternyata karena masalah kompatibilitas NHibernate / WPF. Masalahnya disebabkan oleh cara WPF memeriksa kesetaraan objek. Saya bisa mendapatkan barang-barang saya untuk bekerja dengan menggunakan properti ID objek di properti SelectedValue dan SelectedValuePath.

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

Lihat posting blog dari Chester, The WPF ComboBox - SelectedItem, SelectedValue, dan SelectedValuePath dengan NHibernate , untuk detailnya.

CyberMonk
sumber
1

Saya memiliki masalah serupa di mana SelectedItem tidak pernah diperbarui.

Masalah saya adalah item yang dipilih tidak sama dengan item yang ada dalam daftar. Jadi saya hanya perlu menimpa metode Equals () di MyCustomObject saya dan membandingkan ID dari dua contoh untuk memberi tahu ComboBox bahwa itu adalah objek yang sama.

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