Как сохранить привязку TwoWay к CurrentItem при привязке данных к CollectionViewSource в ComboBox - PullRequest
14 голосов
/ 10 июня 2011

Допустим, мы получили простой класс VM

public class PersonViewModel : Observable
    {
        private Person m_Person= new Person("Mike", "Smith");

        private readonly ObservableCollection<Person> m_AvailablePersons =
            new ObservableCollection<Person>( new List<Person> {
               new Person("Mike", "Smith"),
               new Person("Jake", "Jackson"),                                                               
        });

        public ObservableCollection<Person> AvailablePersons
        {
            get { return m_AvailablePersons; }
        }

        public Person CurrentPerson
        {
            get { return m_Person; }
            set
            {
                m_Person = value;
                NotifyPropertyChanged("CurrentPerson");
            }
        }
    }

Было бы достаточно успешно привязать данные к ComboBox, например, так:

<ComboBox ItemsSource="{Binding AvailablePersons}" 
          SelectedValue="{Binding Path=CurrentPerson, Mode=TwoWay}" />

Обратите внимание, что Person перегружен Equals, и когда я устанавливаю значение CurrentPerson в ViewModel, это заставляет текущий элемент комбинированного списка отображать новое значение.

Теперь допустим, что я хочу добавить в свой вид возможности сортировки, используя CollectionViewSource

 <UserControl.Resources>
        <CollectionViewSource x:Key="PersonsViewSource" Source="{Binding AvailablePersons}">
            <CollectionViewSource.SortDescriptions>
                <scm:SortDescription PropertyName="Surname" Direction="Ascending" />
            </CollectionViewSource.SortDescriptions>
        </CollectionViewSource>
    </UserControl.Resources>

Теперь привязка исходного текста в выпадающих списках будет выглядеть так:

<ComboBox ItemsSource="{Binding Source={StaticResource PersonsViewSource}}"
          SelectedValue="{Binding Path=CurrentPerson, Mode=TwoWay}" />    

И он действительно будет отсортирован (если мы добавим больше элементов, его ясно видно).

Однако, когда мы сейчас изменим CurrentPerson в ВМ (раньше с прозрачным связыванием без CollectionView это работало нормально), это изменение не отображается в связанном ComboBox.

Я полагаю, что после этого, чтобы установить CurrentItem из VM, мы должны каким-то образом получить доступ к View (и мы не идем к View из ViewModel в MVVM), и вызвать MoveCurrentTo метод, чтобы заставить View display изменить CurrentItem.

Таким образом, добавив дополнительные возможности просмотра (сортировку), мы потеряли привязку TwoWay к существующей модели представления, что, я думаю, не является ожидаемым поведением.

Есть ли способ сохранить привязку TwoWay здесь? Или, может быть, я что-то сделал не так.

РЕДАКТИРОВАТЬ: на самом деле ситуация сложнее, чем может показаться, когда я переписываю сеттер CurrentPerson следующим образом:

set
{
    if (m_AvailablePersons.Contains(value)) {
       m_Person = m_AvailablePersons.Where(p => p.Equals(value)).First();
    }
    else throw new ArgumentOutOfRangeException("value");
    NotifyPropertyChanged("CurrentPerson");

}

работает fine!

Его глючное поведение, или есть объяснение? По некоторым причинам, даже если Equals перегружен, требуется ссылочное равенство объекта person.

Я действительно не понимаю, почему для этого нужно ссылочное равенство, поэтому я добавляю щедрость для того, кто может объяснить, почему обычный установщик не работает, когда метод Equal перегружен, что явно может быть видно в "исправлении" кода, который его использует

Ответы [ 4 ]

9 голосов
/ 24 июня 2011

Вас связывают две проблемы, но вы выделили реальную проблему с использованием CollectionViewSource с ComboBox. Я все еще ищу альтернативы, чтобы исправить это «лучшим способом», но ваше исправление setter устраняет проблему по уважительной причине.

Я воспроизвел ваш пример во всех подробностях, чтобы подтвердить проблему и теорию о причине.

Привязка ComboBox к CurrentPerson не использует оператор равенства для нахождения соответствия ЕСЛИ ВЫ ИСПОЛЬЗУЕТЕ SelectedValue INSTEAD OF SelectedItem . Если вы остановите свой override bool Equals(object obj), вы увидите, что при изменении выбора оно не будет достигнуто.

Изменяя свой установщик на следующий, вы находите конкретный соответствующий объект, используя ваш оператор Equals, так что последующее сравнение значений 2 объектов будет работать.

set
{
    if (m_AvailablePersons.Contains(value)) {
       m_Person = m_AvailablePersons.Where(p => p.Equals(value)).First();
    }
    else throw new ArgumentOutOfRangeException("value");
    NotifyPropertyChanged("CurrentPerson");

}

Теперь действительно интересный результат:

Даже если вы измените свой код на использование SelectedItem, он будет работать для обычной привязки к списку, но все равно не будет работать для любой привязки к отсортированному представлению!

Я добавил отладочный вывод в метод Equals, и хотя совпадения были найдены, они были проигнорированы:

public override bool Equals(object obj)
{
    if (obj is Person)
    {
        Person other = obj as Person;
        if (other.Firstname == Firstname && other.Surname == Surname)
        {
            Debug.WriteLine(string.Format("{0} == {1}", other.ToString(), this.ToString()));
            return true;
        }
        else
        {
            Debug.WriteLine(string.Format("{0} <> {1}", other.ToString(), this.ToString()));
            return false;
        }
    }
    return base.Equals(obj);
}

Мой вывод ...

... заключается в том, что за кулисами ComboBox находит совпадение, но из-за наличия CollectionViewSource между ним и необработанными данными он затем игнорирует сопоставление и сравнивает объекты вместо этого (чтобы решить, какой из них был выбран) , Из памяти CollectionViewSource управляет собственным текущим выбранным элементом, , поэтому, если вы не получите точное соответствие объекта, он никогда не будет работать с использованием CollectionViewSource с ComboxBox .

По сути, ваше изменение сеттера работает, потому что оно гарантирует совпадение объектов в CollectionViewSource, которое затем гарантирует совпадение объектов в ComboBox.

Тестовый код

Полный тестовый код приведен ниже для тех, кто хочет играть (извините за взлом кода, но это было только для тестирования, а не MVVM).

Просто создайте новое приложение Silverlight 4 и добавьте следующие файлы / изменения:

PersonViewModel.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
namespace PersonTests
{
    public class PersonViewModel : INotifyPropertyChanged
    {
        private Person m_Person = null;

        private readonly ObservableCollection<Person> m_AvailablePersons =
            new ObservableCollection<Person>(new List<Person> {
               new Person("Mike", "Smith"),
               new Person("Jake", "Jackson"),                                                               
               new Person("Anne", "Aardvark"),                                                               
        });

        public ObservableCollection<Person> AvailablePersons
        {
            get { return m_AvailablePersons; }
        }

        public Person CurrentPerson
        {
            get { return m_Person; }
            set
            {
                if (m_Person != value)
                {
                    m_Person = value;
                    NotifyPropertyChanged("CurrentPerson");
                }
            }

            //set // This works
            //{
            //  if (m_AvailablePersons.Contains(value)) {
            //     m_Person = m_AvailablePersons.Where(p => p.Equals(value)).First();
            //  }
            //  else throw new ArgumentOutOfRangeException("value");
            //  NotifyPropertyChanged("CurrentPerson");
            //}
        }

        private void NotifyPropertyChanged(string name)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }

    public class Person
    {
        public string Firstname { get; set; }
        public string Surname { get; set; }

        public Person(string firstname, string surname)
        {
            this.Firstname = firstname;
            this.Surname = surname;
        }

        public override string ToString()
        {
            return Firstname + "  " + Surname;
        }

        public override bool Equals(object obj)
        {
            if (obj is Person)
            {
                Person other = obj as Person;
                if (other.Firstname == Firstname && other.Surname == Surname)
                {
                    Debug.WriteLine(string.Format("{0} == {1}", other.ToString(), this.ToString()));
                    return true;
                }
                else
                {
                    Debug.WriteLine(string.Format("{0} <> {1}", other.ToString(), this.ToString()));
                    return false;
                }
            }
            return base.Equals(obj);
        }
    }
}

MainPage.xaml

<UserControl x:Class="PersonTests.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:scm="clr-namespace:System.ComponentModel;assembly=System.Windows" mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
    <UserControl.Resources>
        <CollectionViewSource x:Key="PersonsViewSource" Source="{Binding AvailablePersons}">
            <CollectionViewSource.SortDescriptions>
                <scm:SortDescription PropertyName="Surname" Direction="Ascending" />
            </CollectionViewSource.SortDescriptions>
        </CollectionViewSource>
    </UserControl.Resources>
    <StackPanel x:Name="LayoutRoot" Background="LightBlue" Width="150">
        <!--<ComboBox ItemsSource="{Binding AvailablePersons}"
              SelectedItem="{Binding Path=CurrentPerson, Mode=TwoWay}" />-->
        <ComboBox ItemsSource="{Binding Source={StaticResource PersonsViewSource}}"
          SelectedItem="{Binding Path=CurrentPerson, Mode=TwoWay}" />
        <Button Content="Select Mike Smith" Height="23" Name="button1" Click="button1_Click" />
        <Button Content="Select Anne Aardvark" Height="23" Name="button2" Click="button2_Click" />
    </StackPanel>
</UserControl>

MainPage.xaml.cs

using System.Windows;
using System.Windows.Controls;

namespace PersonTests
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
            this.DataContext = new PersonViewModel();
        }

        private void button1_Click(object sender, RoutedEventArgs e)
        {
            (this.DataContext as PersonViewModel).CurrentPerson = new Person("Mike", "Smith");
        }

        private void button2_Click(object sender, RoutedEventArgs e)
        {
            (this.DataContext as PersonViewModel).CurrentPerson = new Person("Anne", "Aardvark");

        }
    }
}
1 голос
/ 27 июня 2011

Рассматривали ли вы использование CollectionView и установите IsSynchronizedWithCurrentItem в выпадающем списке?

Это то, что я бы сделал - вместо того, чтобы иметь свойство CurrentPerson, у вас есть выбранный человек в вашем collectionView.CurrentItem и поле со списком, следующее за currentitem в собрании.

Я использовал collectionview с сортировкой и группировкой без проблем - и с ним вы получаете отличное разделение от интерфейса.

Я бы переместил представление коллекции в код и связал бы его там

public ICollectionView AvailablePersonsView {get; private set;}

в ктор:

AvailablePersonsView = CollectionViewSource.GetDefaultView (AvailablePersons)

0 голосов
/ 22 мая 2012

Я настоятельно рекомендую использовать ComboBoxExtensions Кайлом Макклелланом из Microsoft, найден здесь .

Вы можете объявить источник данных для вашего ComboBox в XAML - и он гораздо более гибок и может использоваться в асинхронных режимах.

По сути, в основном решение НЕ состоит в использовании CollectionViewSource для ComboBoxes. Вы можете выполнить сортировку по запросу на стороне сервера.

0 голосов
/ 10 июня 2011

Привязка TwoWay работает должным образом, но ComboBox не обновляет себя в пользовательском интерфейсе, когда вы устанавливаете SelectedItem или SelectedIndex из кода. Если вам нужна эта функциональность, просто расширьте ComboBox и прослушайте SelectionChanged, унаследованное от Selector или, если вы хотите установить только начальный выбор, сделайте это на Loaded.

...