在数据绑定到ComboBox中的CollectionViewSource时,如何保留CurrentItem的TwoWay绑定

Val*_*zub 15 c# silverlight collectionviewsource silverlight-4.0

假设我们有一个简单的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");
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

成功数据绑定到ComboBox就足够了,例如:

<ComboBox ItemsSource="{Binding AvailablePersons}" 
          SelectedValue="{Binding Path=CurrentPerson, Mode=TwoWay}" />
Run Code Online (Sandbox Code Playgroud)

请注意,Person已经Equals过载,当我在ViewModel中设置CurrentPerson值时,它会导致组合框当前项显示新值.

现在假设我想在我的视图中添加排序功能 CollectionViewSource

 <UserControl.Resources>
        <CollectionViewSource x:Key="PersonsViewSource" Source="{Binding AvailablePersons}">
            <CollectionViewSource.SortDescriptions>
                <scm:SortDescription PropertyName="Surname" Direction="Ascending" />
            </CollectionViewSource.SortDescriptions>
        </CollectionViewSource>
    </UserControl.Resources>
Run Code Online (Sandbox Code Playgroud)

现在组合框项目源绑定将如下所示:

<ComboBox ItemsSource="{Binding Source={StaticResource PersonsViewSource}}"
          SelectedValue="{Binding Path=CurrentPerson, Mode=TwoWay}" />    
Run Code Online (Sandbox Code Playgroud)

它确实会被排序(如果我们添加更多可清楚看到的项目).

但是,当我们CurrentPerson现在更改VM时(在没有CollectionView的情况下进行清除绑定之前它工作正常),此更改不会显示在绑定的ComboBox中.

我相信在那之后为了从VM设置CurrentItem,我们必须以某种方式访问​​View(我们不要从MVVM中的ViewModel查看View),并调用MoveCurrentTo方法来强制View显示currentItem更改.

因此,通过添加额外的视图功能(排序),我们失去了对现有viewModel的TwoWay绑定,我认为这不是预期的行为.

有没有办法在这里保留TwoWay绑定?或许我做错了.

编辑:实际情况更复杂然后它可能会出现,当我重写CurrentPerson setter像这样:

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

}
Run Code Online (Sandbox Code Playgroud)

它有效fine!

它有缺陷的行为,还是有解释?由于某些原因即使Equals重载,也需要引用 person对象的相等性.

我真的不明白为什么它需要引用相等所以我正在为那些可以解释为什么普通的setter不起作用的人添加赏金,当Equal方法重载时可以清楚地看到"修复"使用它的代码

Gon*_*ing 10

有两个问题让你联手,但你突出了使用CollectionViewSource和ComboBox的一个真正的问题.我仍在寻找替代方案,以"更好的方式" 解决这个问题,但是你的setter修复程序有充分的理由避免了这个问题.

我已经全面详细地复制了你的例子,以确认问题和关于原因的理论.

如果您使用SelectedValue INSTEAD OF SelectedItem,则 ComboBox绑定到CurrentPerson不会使用equals运算符来查找匹配项.如果您断点,您override bool Equals(object obj)将在更改选择时看到它没有被击中.

通过改变你的二传手以下内容,你正在寻找一个特定的匹配对象,使用等于运算符,所以比较两个对象的后续值将正常工作.

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

}
Run Code Online (Sandbox Code Playgroud)

现在真正有趣的结果:

即使您将代码更改为使用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);
}
Run Code Online (Sandbox Code Playgroud)

我的结论......

...就是ComboBox在幕后发现匹配,但由于它与原始数据之间存在CollectionViewSource,因此忽略匹配并比较对象(以决定选择哪一个).从内存中,CollectionViewSource管理自己当前选择的项目,因此如果您没有获得精确的对象匹配,则使用带有ComboxBox的CollectionViewSource将无法工作.

基本上你的setter更改是有效的,因为它保证了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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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>
Run Code Online (Sandbox Code Playgroud)

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

        }
    }
}
Run Code Online (Sandbox Code Playgroud)