web-dev-qa-db-fra.com

Liaison d'un ComboBox WPF à une liste personnalisée

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

ComboBox ItemsSource est lié à une propriété d'une classe ViewModel qui répertorie un groupe d'entrées de répertoire RAS en tant que CollectionView. Ensuite, j'ai lié (à des moments différents) les deux SelectedItem ou SelectedValue à une autre propriété du ViewModel. J'ai ajouté un MessageBox à la commande save 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 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 m'intéresse qu'à la valeur de chaîne réelle affichée dans la zone de liste déroulante, pas aux autres propriétés de l'objet, car il s'agit de la valeur que je dois transmettre à RAS lorsque je souhaite établir la connexion VPN, d'où DisplayMemberPath et SelectedValuePath sont tous les deux la propriété Name de ConnectionViewModel. La ComboBox est dans un DataTemplate appliqué à un ItemsControl sur une fenêtre dont DataContext a été défini sur une instance 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 à partir de la commande, la propriété PhonebookEntry contient toujours la valeur initiale, pas la valeur sélectionnée dans la zone de liste déroulante. D'autres instances de TextBox se mettent à jour correctement et s'affichent dans la MessageBox.

Que manque-t-il avec la liaison de données de la ComboBox? J'ai fait beaucoup de recherches et je n'arrive pas à trouver quoi que ce soit que je fais 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 a un CollectionView of ConnectionViewModels. Dans le fichier code-behind MainWindowView.xaml, j'ai défini le DataContext sur MainWindowViewModel. MainWindowView.xaml a un ItemsControl lié à la collection de ConnectionViewModels. J'ai un DataTemplate qui contient le ComboBox ainsi que d'autres TextBox. Les zones de texte sont directement liées aux propriétés de 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 derrière XAML:

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

alors 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 se déplacent entre eux et le ViewModel sans problème. Il n'y a 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 de la variable ItemsControl. Cela me semblerait un peu bête.


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-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 utilisez cet exemple, vous obtiendrez le comportement dont je parle. La zone de texte met correctement à jour sa liaison lorsque vous la modifiez, contrairement à la zone de liste déroulante. C'est très déroutant de voir que la seule chose que j'ai faite est d'introduire un ViewModel parent.

Je suis 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.,

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

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


Pour confirmer mon hypothèse, modifiez la liaison de la zone de texte pour être

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

173
Geoff Bennett

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

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

Je vous ai copié le code et apporté quelques modifications mineures, et cela semble fonctionner correctement. Je peux définir la propriété viewModels PhoneBookEnty et l'élément sélectionné dans la liste déroulante sont modifiés. Je peux également modifier l'élément sélectionné dans la liste déroulante et la propriété PhoneBookEntry de la vue modèles 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 change la propriété PhonebookEntries du ConnectionViewModel en type ReadOnlyCollection, la liaison TwoWay de la propriété SelectedValue sur la liste déroulante fonctionne correctement.

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

System.Windows.Data Avertissement: 50: L'utilisation directe de CollectionView n'est pas totalement prise en charge. Les fonctionnalités de base fonctionnent, malgré quelques inconvénients, 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, alors que DisplayMemberPath spécifie le membre pour l'élément sélectionné et affiché uniquement.

180
Kjetil Watnedal

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

J'ai eu ce qui semblait au départ ê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 données 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 l'article de Chester sur le blog ComboBox WPF - SelectedItem, SelectedValue et SelectedValuePath avec NHibernate, pour plus de détails.

22
CyberMonk

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. Je devais donc simplement redéfinir la méthode Equals () dans MyCustomObject et comparer les ID de ces deux instances pour indiquer à la ComboBox qu'il s'agissait du même objet.

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