Lier un ComboBox WPF à une liste personnalisée

183

J'ai un ComboBox qui ne semble pas mettre à jour SelectedItem / SelectedValue.

Le ComboBox ItemsSource est lié à une propriété sur une classe ViewModel qui répertorie un ensemble d'entrées d'annuaire RAS en tant que CollectionView. Ensuite, j'ai lié (à des moments différents) à la fois le SelectedItemou SelectedValueà une autre propriété du ViewModel. J'ai ajouté un MessageBox dans la commande save pour déboguer les valeurs définies par la liaison de données , mais la liaison SelectedItem/ SelectedValuen'est pas définie.

La classe ViewModel ressemble à ceci:

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 collection _phonebookEntries est en cours d'initialisation dans le constructeur à partir d'un objet métier. Le ComboBox XAML ressemble à ceci:

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

Je ne suis intéressé que par la valeur de chaîne réelle affichée dans la zone de liste déroulante, pas par les autres propriétés de l'objet car c'est la valeur que je dois transmettre à RAS lorsque je veux établir la connexion VPN, donc DisplayMemberPathet SelectedValuePathsont à la fois la propriété Name de le ConnectionViewModel. Le ComboBox est DataTemplateappliqué à un ItemsControlsur une fenêtre dont le DataContext a été défini sur une instance de ViewModel.

Le ComboBox affiche correctement la liste des éléments et je peux en sélectionner un dans l'interface utilisateur sans problème. Cependant, lorsque j'affiche la boîte de message à partir de la commande, la propriété PhonebookEntry contient toujours la valeur initiale, pas la valeur sélectionnée dans le ComboBox. D'autres instances de TextBox se mettent à jour correctement et s'affichent dans MessageBox.

Que me manque-t-il avec la liaison de données à la ComboBox? J'ai fait beaucoup de recherches et je n'arrive pas à trouver quoi que ce soit de mal.


C'est le comportement que je vois, mais cela ne fonctionne pas pour une raison quelconque dans mon contexte particulier.

J'ai un MainWindowViewModel qui a un CollectionViewde ConnectionViewModels. Dans le fichier code-behind MainWindowView.xaml, j'ai défini le DataContext sur MainWindowViewModel. Le MainWindowView.xaml est ItemsControllié à la collection de ConnectionViewModels. J'ai un DataTemplate qui contient le ComboBox ainsi que d'autres TextBoxes. Les TextBoxes sont directement liés aux propriétés du ConnectionViewModel à l'aide de 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; }
}

Le code XAML derrière:

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

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

Les TextBoxes se lient tous correctement et les données se déplacent entre elles et le ViewModel sans problème. Ce n'est que la ComboBox qui ne fonctionne pas.

Vous avez raison dans votre hypothèse concernant la classe PhonebookEntry.

L'hypothèse que je fais est que le DataContext utilisé par mon DataTemplate est automatiquement défini via la hiérarchie de liaison, de sorte que je n'ai pas à le définir explicitement pour chaque élément du ItemsControl. Cela me semblerait un peu ridicule.


Voici une implémentation de test qui illustre le problème, basée sur l'exemple ci-dessus.

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>

Le code derrière :

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

Si vous exécutez cet exemple, vous obtiendrez le comportement dont je parle. Le TextBox met à jour sa liaison correctement lorsque vous le modifiez, mais pas le ComboBox. Très déroutant vu que la seule chose que j'ai faite est d'introduire un ViewModel parent.

Je travaille actuellement sous l'impression qu'un élément lié à l'enfant d'un DataContext a cet enfant comme DataContext. Je ne trouve aucune documentation qui clarifie cela d'une manière ou d'une autre.

C'est à dire,

Window -> DataContext = MainWindowViewModel
..Items -> Lié à DataContext.PhonebookEntries
.... Item -> DataContext = PhonebookEntry (implicitement associé)

Je ne sais pas si cela explique mieux mon hypothèse (?).


Pour confirmer mon hypothèse, modifiez la liaison de la zone de texte en

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

Et cela montrera que la racine de liaison TextBox (que je compare au DataContext) est l'instance ConnectionViewModel.

Geoff Bennett
la source

Réponses:

189

Vous définissez le DisplayMemberPath et le SelectedValuePath sur "Name", donc je suppose que vous avez une classe PhoneBookEntry avec un nom de propriété publique.

Avez-vous défini le DataContext sur votre objet ConnectionViewModel?

J'ai copié votre code et effectué quelques modifications mineures, et cela semble fonctionner correctement. Je peux définir la propriété viewmodels PhoneBookEnty et l'élément sélectionné dans les modifications de la zone de liste déroulante, et je peux modifier l'élément sélectionné dans la zone de liste déroulante et la propriété de vue des modèles PhoneBookEntry est définie correctement.

Voici mon contenu 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>

Et voici mon 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: Le deuxième exemple de Geoff ne semble pas fonctionner, ce qui me semble un peu étrange. Si je modifie la propriété PhonebookEntries sur ConnectionViewModel pour qu'elle soit de type ReadOnlyCollection , la liaison TwoWay de la propriété SelectedValue sur la zone de liste déroulante fonctionne correctement.

Peut-être qu'il y a un problème avec le CollectionView? J'ai remarqué un avertissement dans la console de sortie:

System.Windows.Data Avertissement: 50: L'utilisation directe de CollectionView n'est pas entièrement prise en charge. Les fonctionnalités de base fonctionnent, bien qu'avec quelques inefficacités, mais les fonctionnalités avancées peuvent rencontrer des bogues connus. Pensez à utiliser une classe dérivée pour éviter ces problèmes.

Edit2 (.NET 4.5): Le contenu de DropDownList peut être basé sur ToString () et non sur DisplayMemberPath, tandis que DisplayMemberPath spécifie le membre de l'élément sélectionné et affiché uniquement.

Kjetil Watnedal
la source
1
J'ai également remarqué ce message, mais j'ai supposé que ce qui était couvert aurait été la liaison de données de base. Je suppose que non. :) J'expose maintenant les propriétés en tant que IList <T >et dans le getter de propriété en utilisant _list.AsReadOnly () de la même manière que vous l'avez mentionné. Cela fonctionne comme j'aurais espéré que la méthode originale le ferait. En outre, j'ai pensé que, même si la liaison ItemsSource fonctionnait correctement, j'aurais pu simplement utiliser la propriété Current dans le ViewModel pour accéder à l'élément sélectionné dans le ComboBox. Pourtant, cela ne semble pas aussi naturel que de lier la propriété ComboBoxes SelectedValue / SelectedItem.
Geoff Bennett
3
Je peux confirmer que changer la collection, à laquelle la ItemsSourcepropriété est liée, en une collection en lecture seule le fait fonctionner. Dans mon cas, j'ai dû le changer de ObservableCollectionà ReadOnlyObservableCollection. Des noisettes. Ceci est .NET 3.5 - je ne sais pas s'il est corrigé dans 4.0
ChrisWue
74

Pour lier les données à 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 ressemble à:

public class ComboData
{ 
  public int Id { get; set; } 
  public string Value { get; set; } 
}
Roy
la source
Cette solution ne fonctionne pas pour moi. Le ItemsSource fonctionne correctement, mais les propriétés Path ne redirigent pas correctement vers les valeurs ComboData.
Coneone
3
Idet Valuedoivent être des propriétés , pas un champ de classe, comme:public class ComboData { public int Id { get; set; } public string Value { get; set; } }
Edgar
23

J'ai eu ce qui au début semblait être un problème identique, mais il s'est avéré être dû à un problème de compatibilité NHibernate / WPF. Le problème était dû à la façon dont WPF vérifie l'égalité des objets. J'ai pu faire fonctionner mes affaires en utilisant la propriété ID d'objet dans les propriétés SelectedValue et SelectedValuePath.

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

Voir le billet de blog de Chester, The WPF ComboBox - SelectedItem, SelectedValue et SelectedValuePath with NHibernate , pour plus de détails.

CyberMonk
la source
1

J'ai eu un problème similaire où le SelectedItem n'a jamais été mis à jour.

Mon problème était que l'élément sélectionné n'était pas la même instance que l'élément contenu dans la liste. J'ai donc simplement dû remplacer la méthode Equals () dans mon MyCustomObject et comparer les ID de ces deux instances pour indiquer au ComboBox qu'il s'agissait du même objet.

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