c# - wpf combobox selecteditem




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

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

Le ComboBox ItemsSource est lié à une propriété sur une classe ViewModel qui répertorie un groupe d'entrées d'annuaire RAS en tant que CollectionView. Ensuite, j'ai lié (à des moments séparés) à la fois le SelectedItem ou 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 / 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 suis intéressé par la valeur de chaîne réelle affichée dans la zone de liste déroulante, pas toutes les autres propriétés de l'objet car c'est la valeur que je dois passer à RAS lorsque je veux établir la connexion VPN, donc DisplayMemberPath et SelectedValuePath sont tous deux le nom propriété du ConnectionViewModel. La zone de liste DataTemplate est dans un DataTemplate appliqué à un ItemsControl sur une fenêtre qui DataContext a été définie à une instance ViewModel.

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

Qu'est-ce qui me manque avec la liaison de données de la ComboBox? J'ai fait beaucoup de recherches et n'arrive pas à trouver quelque chose que je fasse 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 CollectionView de ConnectionViewModels. Dans le code-behind MainWindowView.xaml, j'ai défini le DataContext sur MainWindowViewModel. Le MainWindowView.xaml a un ItemsControl lié à la collection de ConnectionViewModels. J'ai un DataTemplate qui contient le ComboBox ainsi que d'autres TextBoxes. Les TextBoxes sont liés 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 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 lient tous correctement, et les données se déplacent entre eux et le ViewModel sans problème. C'est seulement le 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 à travers la hiérarchie de liaison, de sorte que je n'ai pas à le définir explicitement pour chaque élément dans ItemsControl . Cela me semblerait un peu idiot.

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 exécutez cet exemple, vous obtiendrez le comportement dont je parle. La TextBox met à jour sa liaison fine lorsque vous l'éditez, mais le ComboBox ne le fait pas. Très déroutant voyant comme vraiment la seule chose que j'ai fait est d'introduire un parent ViewModel.

Je travaille actuellement sous l'impression qu'un élément lié à l'enfant d'un DataContext a cet enfant en tant que son 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 -> 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 du TextBox à

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

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


J'ai eu ce qui semblait au premier abord être un problème identique, mais il s'est avéré être dû à un problème de compatibilité avec NHibernate / WPF. Le problème a été causé par la façon dont WPF vérifie l'égalité des objets. J'ai été en mesure de faire fonctionner mes trucs 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, le WPF ComboBox - SelectedItem, SelectedValue, et SelectedValuePath avec NHibernate , pour plus de détails.


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. Donc j'ai simplement dû remplacer la méthode Equals () dans mon objet MyCustomObject et comparer les ID de ces deux instances pour dire à la ComboBox que c'est le même objet.

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

Vous définissez DisplayMemberPath et 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 à votre objet ConnectionViewModel?

J'ai copié votre 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 les modifications de la liste déroulante, et je peux modifier l'élément sélectionné dans la liste déroulante et les modèles de vue propriété 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: Geoffs deuxième exemple ne semble pas fonctionner, ce qui me semble un peu étrange. Si je modifie la propriété PhonebookEntries sur le ConnectionViewModel pour être de 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 la 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, bien qu'avec certaines 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.

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





combobox