Wiązanie ComboBox WPF do listy niestandardowej


183

Mam ComboBox, który nie wydaje się aktualizować SelectedItem / SelectedValue.

ComboBox ItemsSource jest powiązany z właściwością klasy ViewModel, która zawiera listę wpisów w książce telefonicznej RAS jako CollectionView. Następnie związałem (w oddzielnych momentach) obie właściwości SelectedItemlub SelectedValueinną właściwość ViewModel. Dodałem MessageBox do komendy save, aby debugować wartości ustawione przez wiązanie danych , ale nie ustawiono wiązania SelectedItem/ SelectedValue.

Klasa ViewModel wygląda mniej więcej tak:

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

Kolekcja _phonebookEntries jest inicjowana w konstruktorze z obiektu biznesowego. ComboBox XAML wygląda mniej więcej tak:

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

Interesuje mnie tylko rzeczywista wartość ciągu wyświetlana w ComboBox, a nie inne właściwości obiektu, ponieważ jest to wartość, którą muszę przekazać do RAS, gdy chcę nawiązać połączenie VPN, stąd DisplayMemberPathi SelectedValuePathsą zarówno właściwością Name ConnectionViewModel. ComboBox jest DataTemplatestosowany ItemsControlw oknie, dla którego DataContext ustawiono na instancję ViewModel.

ComboBox wyświetla listę elementów poprawnie i bez problemu mogę wybrać jeden w interfejsie użytkownika. Jednak gdy wyświetlam okno komunikatu z polecenia, właściwość PhonebookEntry nadal ma w sobie wartość początkową, a nie wybraną wartość z ComboBox. Inne instancje TextBox aktualizują się dobrze i wyświetlają się w MessageBox.

Czego mi brakuje w wiązaniu danych w ComboBox? Przeprowadziłem wiele poszukiwań i nie mogę znaleźć niczego, co robię źle.


Takie zachowanie widzę, jednak z jakiegoś powodu nie działa w moim szczególnym kontekście.

Mam MainWindowViewModel, który ma CollectionViewConnectionViewModels. W pliku MainWindowView.xaml z tyłu kodu ustawiłem DataContext na MainWindowViewModel. MainWindowView.xaml ma ItemsControlpowiązanie z kolekcją ConnectionViewModels. Mam DataTemplate, który przechowuje ComboBox, a także niektóre inne TextBoxy. TextBox są powiązane bezpośrednio z właściwościami ConnectionViewModel za pomocą 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; }
}

Kod XAML za:

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

Następnie 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}" />

Wszystkie TextBoxy wiążą się poprawnie, a dane bez problemu przemieszczają się między nimi a ViewModel. Tylko ComboBox nie działa.

Masz rację w swoich założeniach dotyczących klasy PhonebookEntry.

Zakładam, że DataContext używany przez mój DataTemplate jest automatycznie ustawiany przez hierarchię wiązania, więc nie muszę jawnie ustawiać go dla każdego elementu w ItemsControl. Wydałoby mi się to trochę głupie.


Oto implementacja testowa, która demonstruje problem na podstawie powyższego przykładu.

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>

Kod z opóźnieniem :

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

Jeśli uruchomisz ten przykład, uzyskasz zachowanie, o którym mówię. TextBox aktualizuje swoje wiązanie w porządku podczas edycji, ale ComboBox nie. Bardzo mylące, ponieważ naprawdę jedyne, co zrobiłem, to wprowadzenie nadrzędnego ViewModel.

Obecnie pracuję pod wrażeniem, że element związany z dzieckiem DataContext ma to dziecko jako DataContext. Nie mogę znaleźć żadnej dokumentacji, która wyjaśnia to w ten czy inny sposób.

To znaczy,

Window -> DataContext = MainWindowViewModel
..Items -> Bound to DataContext.PhonebookEntries
.... Pozycja -> DataContext = PhonebookEntry (domyślnie powiązany)

Nie wiem, czy to lepiej tłumaczy moje założenie (?).


Aby potwierdzić moje założenie, zmień powiązanie TextBox na

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

Pokaże to korzeń wiązania TextBox (który porównuję z DataContext) to instancja ConnectionViewModel.

Odpowiedzi:


189

Ustawiłeś DisplayMemberPath i SelectedValuePath na „Name”, więc zakładam, że masz klasę PhoneBookEntry z publiczną właściwością Name.

Czy ustawiłeś DataContext w obiekcie ConnectionViewModel?

Skopiowałem kod i wprowadziłem kilka drobnych modyfikacji i wydaje się, że działa dobrze. Mogę ustawić właściwość Viewmodels PhoneBookEnty, a wybrany element w comboboxu zmienia się, i mogę zmienić wybrany element w combobox, a modele widoku PhoneBookEntry są ustawione poprawnie.

Oto moja zawartość XAML:

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

A oto mój kod za:

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

Edycja: Drugi przykład Geoffsa wydaje się nie działać, co wydaje mi się nieco dziwne. Jeśli zmienię właściwość PhonebookEntries na ConnectionViewModel, aby była typu ReadOnlyCollection , powiązanie TwoWay właściwości SelectedValue na combobox działa poprawnie.

Może występuje problem z CollectionView? Zauważyłem ostrzeżenie w konsoli wyjściowej:

System.Windows.Data Ostrzeżenie: 50: Bezpośrednie korzystanie z CollectionView nie jest w pełni obsługiwane. Podstawowe funkcje działają, choć z pewną nieefektywnością, ale zaawansowane funkcje mogą napotkać znane błędy. Rozważ użycie klasy pochodnej, aby uniknąć tych problemów.

Edycja2 (.NET 4.5): Zawartość DropDownList może być oparta na ToString (), a nie DisplayMemberPath, natomiast DisplayMemberPath określa element członkowski tylko dla wybranego i wyświetlanego elementu.


1
Zauważyłem również tę wiadomość, ale założyłem, że to, co było objęte, wiązałoby podstawowe dane. Nie sądzę. :) Teraz ujawniam właściwości jako IList <T.> ujawniam oraz w narzędziu do pobierania właściwości za pomocą _list.AsReadOnly () podobnie jak wspomniano. Działa tak, jak chciałbym mieć oryginalną metodę. Przyszło mi też do głowy, że gdy powiązanie ItemsSource działa dobrze, mogłem po prostu użyć właściwości Current w ViewModel, aby uzyskać dostęp do wybranego elementu w ComboBox. Mimo to nie wydaje się tak naturalny, jak wiązanie właściwości ComboBoxes SelectedValue / SelectedItem.
Geoff Bennett

3
Mogę potwierdzić, że zmiana kolekcji, z którą ItemsSourcezwiązana jest właściwość, na kolekcję tylko do odczytu, sprawia, że ​​działa. W moim przypadku musiałem to zmienić z ObservableCollectionna ReadOnlyObservableCollection. Orzechy. To jest .NET 3.5 - nie jestem pewien, czy został naprawiony w wersji 4.0
ChrisWue

74

Aby powiązać dane z 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 wygląda następująco:

public class ComboData
{ 
  public int Id { get; set; } 
  public string Value { get; set; } 
}

To rozwiązanie nie działa dla mnie. ItemsSource działa dobrze, ale właściwości ścieżki nie przekierowują poprawnie do wartości ComboData.
Stożek

3
Idi Valuemuszą być właściwościami , a nie polami klas, takimi jak:public class ComboData { public int Id { get; set; } public string Value { get; set; } }
Edgar

23

Miałem coś, co na początku wydawało się identycznym problemem, ale okazało się, że jest to spowodowane problemem kompatybilności NHibernate / WPF. Problem był spowodowany sposobem, w jaki WPF sprawdza równość obiektów. Udało mi się uruchomić moje rzeczy, używając właściwości ID obiektu we właściwościach SelectedValue i SelectedValuePath.

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

Szczegółowe informacje można znaleźć w blogu Chester, The WPF ComboBox - SelectedItem, SelectedValue i SelectedValuePath z NHibernate .


1

Miałem podobny problem, w którym SelectedItem nigdy nie był aktualizowany.

Mój problem polegał na tym, że wybrany element nie był tym samym wystąpieniem, co element zawarty na liście. Musiałem więc po prostu przesłonić metodę Equals () w moim MyCustomObject i porównać identyfikatory tych dwóch instancji, aby powiedzieć ComboBox, że jest to ten sam obiekt.

public override bool Equals(object obj)
{
    return this.Id == (obj as MyCustomObject).Id;
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.