Enlace de un ComboBox de WPF a una lista personalizada

183

Tengo un ComboBox que no parece actualizar SelectedItem / SelectedValue.

ComboBox ItemsSource está vinculado a una propiedad en una clase ViewModel que enumera un montón de entradas de la agenda telefónica RAS como CollectionView. Luego he vinculado (en momentos separados) tanto la propiedad SelectedItemcomo SelectedValuea otra de ViewModel. He agregado un cuadro de mensaje en el comando guardar para depurar los valores establecidos por el enlace de datos, pero el enlace SelectedItem/ SelectedValueno se está configurando.

La clase ViewModel se parece a esto:

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 colección _phonebookEntries se está inicializando en el constructor desde un objeto comercial. El ComboBox XAML se parece a esto:

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

Solo estoy interesado en el valor de cadena real que se muestra en el cuadro combinado, no en ninguna otra propiedad del objeto, ya que este es el valor que necesito pasar a RAS cuando quiero hacer la conexión VPN, por lo tanto, DisplayMemberPathy SelectedValuePathson propiedad del nombre de ConnectionViewModel. El ComboBox se DataTemplateaplica a un ItemsControlen una ventana cuyo DataContext se ha establecido en una instancia de ViewModel.

ComboBox muestra la lista de elementos correctamente, y puedo seleccionar uno en la interfaz de usuario sin ningún problema. Sin embargo, cuando visualizo el cuadro de mensaje del comando, la propiedad PhonebookEntry todavía tiene el valor inicial, no el valor seleccionado de ComboBox. Otras instancias de TextBox se actualizan bien y se muestran en el MessageBox.

¿Qué me falta con el enlace de datos del ComboBox? He buscado mucho y parece que no puedo encontrar nada que esté haciendo mal.


Este es el comportamiento que estoy viendo, sin embargo, no funciona por alguna razón en mi contexto particular.

Tengo un MainWindowViewModel que tiene uno CollectionViewde ConnectionViewModels. En el código subyacente del archivo MainWindowView.xaml, configuré el DataContext en MainWindowViewModel. MainWindowView.xaml tiene un ItemsControlenlace a la colección de ConnectionViewModels. Tengo un DataTemplate que contiene el ComboBox, así como algunos otros TextBoxes. Los cuadros de texto están vinculados directamente a las propiedades de ConnectionViewModel utilizando 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; }
}

El código subyacente de XAML:

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

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

Todos los cuadros de texto se enlazan correctamente y los datos se mueven entre ellos y ViewModel sin problemas. Solo el ComboBox no funciona.

Tiene razón en su suposición con respecto a la clase PhonebookEntry.

La suposición que estoy haciendo es que el DataContext utilizado por mi DataTemplate se establece automáticamente a través de la jerarquía de enlace, por lo que no tengo que establecerlo explícitamente para cada elemento en el ItemsControl. Eso me parecería un poco tonto.


Aquí hay una implementación de prueba que demuestra el problema, basado en el ejemplo anterior.

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>

El código subyacente :

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 ejecuta ese ejemplo, obtendrá el comportamiento del que estoy hablando. TextBox actualiza su enlace bien cuando lo edita, pero ComboBox no. Muy confuso, ya que lo único que he hecho es presentar un ViewModel padre.

Actualmente estoy trabajando bajo la impresión de que un elemento vinculado al elemento secundario de un DataContext tiene ese elemento secundario como su DataContext. No puedo encontrar ninguna documentación que aclare esto de una forma u otra.

Es decir,

Ventana -> DataContext = MainWindowViewModel
..Items -> Vinculado a DataContext.PhonebookEntries
.... Elemento -> DataContext = PhonebookEntry (asociado implícitamente)

No sé si eso explica mi suposición mejor (?).


Para confirmar mi suposición, cambie el enlace de TextBox para que sea

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

Y esto mostrará que la raíz de enlace TextBox (que estoy comparando con el DataContext) es la instancia de ConnectionViewModel.

Geoff Bennett
fuente

Respuestas:

189

Establece DisplayMemberPath y SelectedValuePath en "Nombre", por lo que supongo que tiene una clase PhoneBookEntry con un nombre de propiedad pública.

¿Ha configurado el DataContext a su objeto ConnectionViewModel?

Copié su código e hice algunas modificaciones menores, y parece funcionar bien. Puedo configurar la propiedad viewmodels PhoneBookEnty y el elemento seleccionado en el cuadro combinado cambia, y puedo cambiar el elemento seleccionado en el cuadro combinado y la vista de modelos de propiedad PhoneBookEntry está configurada correctamente.

Aquí está mi contenido 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>

Y aquí está mi código subyacente:

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

Editar: el segundo ejemplo de Geoff no parece funcionar, lo que me parece un poco extraño. Si cambio la propiedad PhonebookEntries en ConnectionViewModel para que sea del tipo ReadOnlyCollection , el enlace TwoWay de la propiedad SelectedValue en el cuadro combinado funciona bien.

Tal vez hay un problema con el CollectionView? Noté una advertencia en la consola de salida:

System.Windows.Data Advertencia: 50: el uso de CollectionView directamente no es totalmente compatible. Las funciones básicas funcionan, aunque con algunas ineficiencias, pero las funciones avanzadas pueden encontrar errores conocidos. Considere usar una clase derivada para evitar estos problemas.

Edit2 (.NET 4.5): el contenido de DropDownList puede basarse en ToString () y no en DisplayMemberPath, mientras que DisplayMemberPath especifica el miembro solo para el elemento seleccionado y mostrado.

Kjetil Watnedal
fuente
1
También noté ese mensaje, pero asumí que lo que estaba cubierto habría sido un enlace de datos básico. Supongo que no. :) Ahora expongo las propiedades como IList <T >y en el captador de propiedades usando _list.AsReadOnly () de forma similar a la que mencionaste. Está funcionando como hubiera esperado que hubiera tenido el método original. Además, me pasó por la mente que, aunque el enlace ItemsSource funcionaba bien, podría haber utilizado la propiedad Current en ViewModel para acceder al elemento seleccionado en ComboBox. Aún así, no se siente tan natural como vincular la propiedad ComboBoxes SelectedValue / SelectedItem.
Geoff Bennett
3
Puedo confirmar que cambiar la colección, a la que ItemsSourceestá vinculada la propiedad, por una colección de solo lectura hace que funcione. En mi caso tuve que cambiarlo de ObservableCollectiona ReadOnlyObservableCollection. Nueces. Esto es .NET 3.5 - no estoy seguro si está arreglado en 4.0
ChrisWue
74

Para vincular los datos 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 se parece a:

public class ComboData
{ 
  public int Id { get; set; } 
  public string Value { get; set; } 
}
Roy
fuente
Esta solución no funciona para mí. ItemsSource funciona bien, pero las propiedades de Path no están redirigiendo correctamente a los valores de ComboData.
Coneone
3
Idy Valuetienen que ser propiedades , no un campo de clase, como:public class ComboData { public int Id { get; set; } public string Value { get; set; } }
Edgar
23

Tenía lo que al principio parecía ser un problema idéntico, pero resultó ser debido a un problema de compatibilidad con NHibernate / WPF. El problema fue causado por la forma en que WPF verifica la igualdad de objetos. Pude hacer que mis cosas funcionen utilizando la propiedad ID de objeto en las propiedades SelectedValue y SelectedValuePath.

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

Consulte la publicación de blog de Chester, The WPF ComboBox - SelectedItem, SelectedValue y SelectedValuePath con NHibernate , para obtener más detalles.

CyberMonk
fuente
1

Tuve un problema similar donde el SelectedItem nunca se actualizó.

Mi problema fue que el elemento seleccionado no era la misma instancia que el elemento contenido en la lista. Entonces, simplemente tuve que anular el método Equals () en mi MyCustomObject y comparar los ID de esas dos instancias para decirle al ComboBox que es el mismo objeto.

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