Подтвердить что ты не робот

Как сохранить привязку TwoWay к CurrentItem при привязке данных к CollectionViewSource в ComboBox

Допустим, мы получили простой класс 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, он вызывает текущее значение элемента combobox для отображения нового значения.

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

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

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

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

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

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

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

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

Есть ли способ сохранить привязку 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 перегружен, для него требуется ссылочное равенство.

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

4b9b3361

Ответ 1

На вас есть две проблемы, но вы подчеркнули реальную проблему с использованием CollectionViewSource с помощью ComboBox. Я по-прежнему ищу альтернативы, чтобы исправить это "лучшим способом", но ваше исправление сеттера устраняет проблему по уважительной причине.

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

Связывание ComboBox с CurrentPerson не использует оператор equals для поиска соответствия ЕСЛИ ВЫ ИСПОЛЬЗОВАТЬ SelectedValue ВМЕСТО ИЗ ВЫБОРНОГО ИМЯ. Если вы остановите свой override bool Equals(object obj), вы увидите, что он не пострадает, когда вы меняете выделение.

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

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

        }
    }
}

Ответ 2

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

Ответ 3

Считаете ли вы использование CollectionView и установите IsSynchronizedWithCurrentItem в combobox?

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

Я использовал коллекцию с сортировкой и группировкой без проблем - и вы получите приятную развязку от ui с ней.

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

public ICollectionView AvailablePersonsView {get; private set;}

в ctor:

ДоступноPersonsView = CollectionViewSource.GetDefaultView(AvailablePersons)

Ответ 4

Я очень рекомендую использовать ComboBoxExtensions от Kyle McClellan из Microsoft, здесь здесь.

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

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