Vinculando uma caixa de combinação WPF a uma lista personalizada

183

Eu tenho uma caixa de combinação que parece não atualizar o SelectedItem / SelectedValue.

O ComboBox ItemsSource está vinculado a uma propriedade em uma classe ViewModel que lista um monte de entradas da agenda telefônica do RAS como um CollectionView. Em seguida, vinculei (em momentos separados) a SelectedItemou SelectedValuea outra propriedade do ViewModel. Eu adicionei uma MessageBox ao comando save para depurar os valores definidos pela ligação de dados, mas a ligação SelectedItem/ SelectedValuenão está sendo definida.

A classe ViewModel se parece com isso:

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

A coleção _phonebookEntries está sendo inicializada no construtor a partir de um objeto de negócios. O ComboBox XAML é mais ou menos assim:

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

Eu só estou interessado no valor de cadeia real exibido na caixa de combinação, e não quaisquer outras propriedades do objeto como este é o valor que eu preciso para passar através de RAS quando eu quero fazer a conexão VPN, portanto, DisplayMemberPathe SelectedValuePathestão ambos a propriedade Name o ConnectionViewModel. O ComboBox é DataTemplateaplicado a um ItemsControlem uma janela cujo DataContext foi definido como uma instância do ViewModel.

O ComboBox exibe a lista de itens corretamente e eu posso selecionar um na interface do usuário sem nenhum problema. No entanto, quando eu exibir a caixa de mensagem do comando, a propriedade PhonebookEntry ainda possui o valor inicial, não o valor selecionado na ComboBox. Outras instâncias do TextBox estão sendo atualizadas corretamente e exibidas na MessageBox.

O que estou perdendo com a ligação de dados da ComboBox? Pesquisei bastante e não consigo encontrar nada do que estou fazendo de errado.


Esse é o comportamento que estou vendo, no entanto, não está funcionando por algum motivo no meu contexto particular.

Eu tenho um MainWindowViewModel que tem um CollectionViewde ConnectionViewModels. No arquivo MainWindowView.xaml, code-behind, defino o DataContext como MainWindowViewModel. O MainWindowView.xaml tem um ItemsControllimite para a coleção de ConnectionViewModels. Eu tenho um DataTemplate que contém o ComboBox, bem como alguns outros TextBoxes. Os TextBoxes são vinculados diretamente às propriedades do ConnectionViewModel usando 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; }
}

O código XAML por trás:

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

Então 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 os TextBoxes se vinculam corretamente e os dados são movidos entre eles e o ViewModel sem problemas. É apenas a ComboBox que não está funcionando.

Você está correto em sua suposição sobre a classe PhonebookEntry.

A suposição que estou assumindo é que o DataContext usado pelo meu DataTemplate é definido automaticamente através da hierarquia de ligação, para que eu não precise defini-lo explicitamente para cada item no ItemsControl. Isso me pareceria um pouco bobo.


Aqui está uma implementação de teste que demonstra o problema, com base no exemplo acima.

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>

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

Se você executar esse exemplo, obterá o comportamento de que estou falando. O TextBox atualiza bem sua ligação quando você o edita, mas o ComboBox não. Muito confuso, vendo que realmente a única coisa que fiz foi apresentar um ViewModel pai.

Atualmente, estou trabalhando com a impressão de que um item vinculado ao filho de um DataContext tem esse filho como seu DataContext. Não consigo encontrar nenhuma documentação que esclareça isso de uma maneira ou de outra.

Ou seja,

Janela -> DataContext = MainWindowViewModel
..Items -> Vinculado a DataContext.PhonebookEntries
.... Item -> DataContext = PhonebookEntry (implicitamente associado)

Não sei se isso explica melhor minha suposição (?).


Para confirmar minha suposição, altere a ligação do TextBox para

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

E isso mostrará que a raiz de ligação do TextBox (que estou comparando com o DataContext) é a instância do ConnectionViewModel.

Geoff Bennett
fonte

Respostas:

189

Você define DisplayMemberPath e SelectedValuePath como "Name", portanto, suponho que você tenha uma classe PhoneBookEntry com uma propriedade pública Name.

Você definiu o DataContext como seu objeto ConnectionViewModel?

Copiei o código e fiz algumas pequenas modificações, e parece funcionar bem. Posso definir a propriedade PhonemodEnty do viewmodels e o item selecionado na caixa de combinação é alterado. Posso alterar o item selecionado na caixa de combinação e a propriedade PhoneBookEntry dos modelos de exibição está definida corretamente.

Aqui está o meu conteúdo 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>

E aqui está o meu 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 segundo exemplo não parece funcionar, o que parece um pouco estranho para mim. Se eu alterar a propriedade PhonebookEntries no ConnectionViewModel para ser do tipo ReadOnlyCollection , a ligação TwoWay da propriedade SelectedValue na caixa de combinação funcionará bem.

Talvez haja um problema com o CollectionView? Notei um aviso no console de saída:

System.Windows.Data Aviso: 50: O uso do CollectionView diretamente não é totalmente suportado. Os recursos básicos funcionam, embora com algumas ineficiências, mas os recursos avançados podem encontrar bugs conhecidos. Considere usar uma classe derivada para evitar esses problemas.

Edit2 (.NET 4.5): O conteúdo do DropDownList pode ser baseado em ToString () e não em DisplayMemberPath, enquanto DisplayMemberPath especifica o membro apenas para o item selecionado e exibido.

Kjetil Watnedal
fonte
1
Também notei essa mensagem, mas presumi que o que foi coberto teria sido uma ligação básica de dados. Eu acho que não. :) Agora estou expondo as propriedades como IList <T >e no getter de propriedades usando _list.AsReadOnly () semelhante à maneira como você mencionou. Está funcionando como eu esperava que o método original funcionasse. Além disso, me ocorreu que, enquanto a ligação ItemsSource estava funcionando bem, eu poderia ter usado a propriedade Current no ViewModel para acessar o item selecionado na ComboBox. Ainda assim, não parece tão natural quanto vincular a propriedade ComboBoxes SelectedValue / SelectedItem.
Geoff Bennett
3
Posso confirmar que alterar a coleção, à qual a ItemsSourcepropriedade está vinculada, a uma coleção somente leitura faz com que funcione. No meu caso, tive que mudar de ObservableCollectionpara ReadOnlyObservableCollection. Nozes. Este é o .NET 3.5 - não tenho certeza se está corrigido no 4.0
ChrisWue
74

Para vincular os dados à 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";

O ComboData se parece com:

public class ComboData
{ 
  public int Id { get; set; } 
  public string Value { get; set; } 
}
Roy
fonte
Esta solução não funciona para mim. O ItemsSource funciona bem, mas as propriedades Path não estão redirecionando corretamente para os valores ComboData.
Coneone
3
Ide Valuetem que ser propriedades , não campo de classe, como:public class ComboData { public int Id { get; set; } public string Value { get; set; } }
Edgar
23

Eu tive o que inicialmente parecia ser um problema idêntico, mas acabou por ser devido a um problema de compatibilidade do NHibernate / WPF. O problema foi causado pela maneira como o WPF verifica a igualdade de objetos. Consegui fazer minhas coisas funcionarem usando a propriedade ID do objeto nas propriedades SelectedValue e SelectedValuePath.

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

Consulte a postagem do blog de Chester, The ComboBox do WPF - SelectedItem, SelectedValue e SelectedValuePath com NHibernate , para obter detalhes.

CyberMonk
fonte
1

Eu tive um problema semelhante em que o SelectedItem nunca foi atualizado.

Meu problema era que o item selecionado não era a mesma instância que o item contido na lista. Então, eu simplesmente tive que substituir o método Equals () no meu MyCustomObject e comparar os IDs dessas duas instâncias para informar à ComboBox que é o mesmo objeto.

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