Associare un ComboBox WPF a un elenco personalizzato


183

Ho un ComboBox che non sembra aggiornare SelectedItem / SelectedValue.

ComboBox ItemsSource è associato a una proprietà in una classe ViewModel che elenca un gruppo di voci della rubrica RAS come CollectionView. Poi ho legato (in tempi diversi) sia la SelectedItemo SelectedValuead un'altra proprietà del ViewModel. Ho aggiunto un MessageBox nel comando save per eseguire il debug dei valori impostati dal database, ma il SelectedItem/ SelectedValuebinding non viene impostato.

La classe ViewModel ha un aspetto simile al seguente:

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

La collezione _phonebookEntries è stata inizializzata nel costruttore da un oggetto business. ComboBox XAML è simile al seguente:

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

Sono interessato solo al valore della stringa effettiva visualizzato nel ComboBox, non a qualsiasi altra proprietà dell'oggetto in quanto questo è il valore che devo trasmettere a RAS quando voglio effettuare la connessione VPN, quindi DisplayMemberPathe SelectedValuePathsono entrambi proprietà Name di il ConnectionViewModel. ComboBox è DataTemplateapplicato a ItemsControlsu una finestra in cui DataContext è stato impostato su un'istanza ViewModel.

ComboBox visualizza correttamente l'elenco degli elementi e posso selezionarne uno nell'interfaccia utente senza problemi. Tuttavia, quando visualizzo la finestra di messaggio dal comando, la proprietà PhonebookEntry contiene ancora il valore iniziale, non il valore selezionato da ComboBox. Altre istanze di TextBox si stanno aggiornando correttamente e vengono visualizzate in MessageBox.

Cosa mi manca con il databinding del ComboBox? Ho fatto molte ricerche e non riesco a trovare nulla di sbagliato.


Questo è il comportamento che sto vedendo, tuttavia non funziona per qualche motivo nel mio contesto particolare.

Ho un MainWindowViewModel che ha un CollectionViewConnectionViewModels. Nel file MainWindowView.xaml code-behind, ho impostato DataContext su MainWindowViewModel. MainWindowView.xaml è ItemsControlassociato alla raccolta di ConnectionViewModels. Ho un DataTemplate che contiene ComboBox e alcuni altri TextBox. Le caselle di testo sono associate direttamente alle proprietà di ConnectionViewModel mediante 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; }
}

Il code-behind XAML:

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

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

Le caselle di testo si legano tutte correttamente e i dati si spostano tra loro e ViewModel senza problemi. È solo il ComboBox che non funziona.

Sei corretto nel tuo assunto riguardo alla classe PhonebookEntry.

Il presupposto che sto facendo è che il DataContext utilizzato dal mio DataTemplate sia impostato automaticamente attraverso la gerarchia di associazione, in modo da non doverlo impostare esplicitamente per ogni elemento in ItemsControl. Mi sembrerebbe un po 'sciocco.


Ecco un'implementazione di prova che dimostra il problema, basato sull'esempio sopra.

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>

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

Se esegui questo esempio, otterrai il comportamento di cui sto parlando. TextBox aggiorna la sua multa vincolante quando la modifichi, ma ComboBox no. Molto confuso visto che davvero l'unica cosa che ho fatto è introdurre un ViewModel genitore.

Attualmente sto lavorando con l'impressione che un elemento associato al figlio di un DataContext abbia quel figlio come DataContext. Non riesco a trovare alcuna documentazione che chiarisca questo in un modo o nell'altro.

Vale a dire,

Window -> DataContext = MainWindowViewModel
..Items -> Bound to DataContext.PhonebookEntries
.... Item -> DataContext = PhonebookEntry (implicitamente associato)

Non so se questo spieghi meglio la mia ipotesi (?).


Per confermare la mia ipotesi, modificare l'associazione di TextBox in modo che sia

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

E questo mostrerà la radice dell'associazione TextBox (che sto confrontando con DataContext) è l'istanza di ConnectionViewModel.

Risposte:


189

Impostare DisplayMemberPath e SelectedValuePath su "Nome", quindi suppongo che tu abbia una classe PhoneBookEntry con un nome di proprietà pubblica.

Hai impostato DataContext sull'oggetto ConnectionViewModel?

Ho copiato il tuo codice e apportato alcune piccole modifiche, e sembra funzionare bene. Posso impostare la proprietà PhoneBookEnty dei modelli di visualizzazione e l'elemento selezionato nella casella combinata cambia e posso modificare l'elemento selezionato nella casella combinata e la proprietà PhoneBookEntry dei modelli di visualizzazione è impostata correttamente.

Ecco il mio contenuto 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>

Ed ecco il mio 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;
    }
}

Modifica: il secondo esempio di Geoffs non sembra funzionare, il che mi sembra un po 'strano. Se cambio la proprietà PhonebookEntries su ConnectionViewModel in modo che sia di tipo ReadOnlyCollection , l'associazione TwoWay della proprietà SelectedValue sulla casella combinata funziona correttamente.

Forse c'è un problema con CollectionView? Ho notato un avviso nella console di output:

System.Windows.Data Avvertenza: 50: L'uso diretto di CollectionView non è completamente supportato. Le funzionalità di base funzionano, sebbene con alcune inefficienze, ma le funzionalità avanzate potrebbero riscontrare bug noti. Prendi in considerazione l'utilizzo di una classe derivata per evitare questi problemi.

Edit2 (.NET 4.5): il contenuto di DropDownList può essere basato su ToString () e non su DisplayMemberPath, mentre DisplayMemberPath specifica il membro solo per l'elemento selezionato e visualizzato.


1
Ho notato anche quel messaggio, ma ho pensato che ciò che era coperto sarebbe stato un legame di base dei dati. Non credo. :) Ora sto esponendo le proprietà come IList <T >e nel getter di proprietà usando _list.AsReadOnly () simile al modo in cui hai menzionato. Funziona come avrei sperato avrebbe avuto il metodo originale. Inoltre, mi è venuto in mente che mentre l'associazione ItemsSource funzionava bene, avrei potuto usare la proprietà Current in ViewModel per accedere all'elemento selezionato in ComboBox. Tuttavia, non sembra naturale quanto vincolante la proprietà ComboBoxes SelectedValue / SelectedItem.
Geoff Bennett,

3
Posso confermare che cambiando la raccolta, alla quale ItemsSourceè vincolata la proprietà, una raccolta di sola lettura la fa funzionare. Nel mio caso ho dovuto cambiarlo da ObservableCollectiona ReadOnlyObservableCollection. Noccioline. Questo è .NET 3.5 - non sono sicuro se è stato risolto in 4.0
ChrisWue

74

Per associare i dati a 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 è simile a:

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

Questa soluzione non funziona per me. ItemsSource funziona correttamente, ma le proprietà Path non reindirizzano correttamente ai valori ComboData.
Coneone,

3
Ide Valuedevono essere proprietà , non campo di classe, come:public class ComboData { public int Id { get; set; } public string Value { get; set; } }
Edgar

23

Ho avuto quello che all'inizio sembrava essere un problema identico, ma si è rivelato essere dovuto a un problema di compatibilità NHibernate / WPF. Il problema è stato causato dal modo in cui WPF verifica l'uguaglianza degli oggetti. Sono stato in grado di far funzionare le mie cose usando la proprietà ID oggetto nelle proprietà SelectedValue e SelectedValuePath.

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

Vedi il post sul blog di Chester, The WPF ComboBox - SelectedItem, SelectedValue e SelectedValuePath con NHibernate , per i dettagli.


1

Ho avuto un problema simile in cui SelectedItem non è mai stato aggiornato.

Il mio problema era che l'elemento selezionato non era la stessa istanza dell'elemento contenuto nell'elenco. Quindi ho dovuto semplicemente sostituire il metodo Equals () nel mio MyCustomObject e confrontare gli ID di quelle due istanze per dire a ComboBox che è lo stesso oggetto.

public override bool Equals(object obj)
{
    return this.Id == (obj as MyCustomObject).Id;
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.