207 votes

Lier une ComboBox WPF à une liste personnalisée

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

La ComboBox ItemsSource est liée à une propriété d'une classe ViewModel qui liste un ensemble d'entrées de répertoire RAS sous la forme d'une CollectionView, puis j'ai lié (à des moments différents) les SelectedItem ou SelectedValue à une autre propriété du ViewModel. J'ai ajouté un MessageBox dans la commande de sauvegarde pour déboguer les valeurs définies par la liaison de données, mais la liaison SelectedItem/SelectedValue n'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 initialisée dans le constructeur à partir d'un objet métier. Le XAML de la ComboBox ressemble à ceci :

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

Je ne suis intéressé que par la valeur réelle de la chaîne affichée dans la ComboBox, et non 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, d'où le fait que DisplayMemberPath et SelectedValuePath sont tous deux la propriété Name du ConnectionViewModel. La ComboBox se trouve dans un DataTemplate appliqué à un ItemsControl sur une fenêtre dont le DataContext a été défini sur une instance de ViewModel.

La 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 de la commande, la propriété PhonebookEntry contient toujours la valeur initiale, et non la valeur sélectionnée dans la ComboBox. Les autres instances de TextBox se mettent à jour correctement et s'affichent dans la boîte de message.

Qu'est-ce que je rate avec la liaison de données de la ComboBox ? J'ai fait beaucoup de recherches et je n'ai pas trouvé ce que je faisais de mal.


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

J'ai un MainWindowViewModel qui possède une CollectionView de ConnectionViewModels. Dans le code-behind du fichier MainWindowView.xaml, j'ai défini le DataContext sur le MainWindowViewModel. Le fichier MainWindowView.xaml comporte un ItemsControl lié à la collection de ConnectionViewModels. J'ai un DataTemplate qui contient la ComboBox ainsi que d'autres TextBoxes. Les boîtes de texte sont liées directement aux propriétés du ConnectionViewModel en utilisant 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-behind XAML :

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 TextBox se lient tous correctement, et les données circulent entre eux et le ViewModel sans problème. C'est seulement la ComboBox qui ne fonctionne pas.

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

Je suppose que le DataContext utilisé par mon DataTemplate est automatiquement défini par la hiérarchie de liaison, de sorte que je n'ai pas à le définir explicitement pour chaque élément du ItemsControl. Cela me semble un peu stupide.

220voto

Kjetil Watnedal Points 2917

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

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

J'ai copié votre code et apporté quelques modifications mineures, et il semble fonctionner correctement. Je peux définir la propriété PhoneBookEnty des modèles de vue et l'élément sélectionné dans la combobox change, et je peux changer l'élément sélectionné dans la combobox et la propriété PhoneBookEntry des modèles de vue est définie correctement.

Voici mon 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 derrière :

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 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 changer la propriété PhonebookEntries du ConnectionViewModel pour qu'elle soit de type ReadOnlyCollection La liaison bidirectionnelle de la propriété SelectedValue de la liste déroulante fonctionne bien.

Peut-être y a-t-il un problème avec le CollectionView ? J'ai remarqué un avertissement dans la console de sortie : "System.Windows.Data Warning : 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. Envisagez d'utiliser une classe dérivée pour éviter ces problèmes."

90voto

Roy Points 321

Pour lier les données à la 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";

28voto

CyberMonk Points 493

J'ai eu ce qui semblait être au départ 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 réussi à faire fonctionner mon système en utilisant la propriété ID de l'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 lien suivant de Chester pour plus de détails : La ComboBox WPF - SelectedItem, SelectedValue et SelectedValuePath avec NHibernate

2voto

Geoff Bennett Points 690

Voici une mise en œuvre de test qui démontre 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-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; }
        }
    }
}

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

J'ai actuellement l'impression qu'un élément lié à l'enfant d'un DataContext a cet enfant comme DataContext. Je n'arrive pas à trouver de documentation qui clarifie ce point, dans un sens ou dans l'autre.

I.e,

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

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

2voto

Geoff Bennett Points 690

Pour confirmer mon hypothèse, changez la liaison de la TextBox pour qu'elle soit

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

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X