数据绑定到 ComboBox 中的 CollectionViewSource 时如何保留 CurrentItem 的双向绑定 [英] How to preserve TwoWay binding of CurrentItem when databinding to CollectionViewSource in ComboBox

查看:19
本文介绍了数据绑定到 ComboBox 中的 CollectionViewSource 时如何保留 CurrentItem 的双向绑定的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

假设我们有一个简单的 VM 类

公共类 PersonViewModel : Observable{私人人 m_Person= 新人(迈克",史密斯");私有只读 ObservableCollectionm_AvailablePersons =new ObservableCollection( new List {新人(迈克",史密斯"),新人(杰克",杰克逊"),});公共 ObservableCollection有空人员{得到 { 返回 m_AvailablePersons;}}公众人物 CurrentPerson{得到 { 返回 m_Person;}放{m_Person = 值;NotifyPropertyChanged("CurrentPerson");}}}

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

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

现在假设我想使用 CollectionViewSource

为我的视图添加排序功能

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

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

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

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

我相信之后为了从VM设置CurrentItem我们必须以某种方式访问​​View(并且我们不会从MVVM中的ViewModel转到View),并调用MoveCurrentTo方法来强制View显示当前项目更改.

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

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

实际上情况比它可能出现的更复杂,当我像这样重写 CurrentPerson setter 时:

set{如果(m_AvailablePersons.Contains(值)){m_Person = m_AvailablePersons.Where(p => p.Equals(value)).First();}否则抛出新的 ArgumentOutOfRangeException("value");NotifyPropertyChanged("CurrentPerson");}

<块引用>

效果很好没问题

它的错误行为,或者有什么解释吗?由于某些原因,即使 Equals 被重载,它也需要 person 对象的引用相等.

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

解决方案

有 2 个问题困扰着您,但您已经突出了将 CollectionViewSource 与 ComboBox 一起使用的真正问题.我仍在寻找以更好的方式"解决此问题的替代方法,但是您的 setter 修复程序有充分的理由避免了该问题.

我已经完整详细地复制了您的示例以确认问题和有关原因的理论.

ComboBox 绑定到 CurrentPerson 不使用等于运算符来查找匹配项如果您使用 SelectedValue 代替 SelectedItem.如果你断点你的 override bool Equals(object obj) 你会看到当你改变选择时它没有被击中.

通过将您的 setter 更改为以下内容,您将使用 Equals 运算符查找特定的匹配对象,因此后续 2 个对象的值比较将起作用.

set{如果(m_AvailablePersons.Contains(值)){m_Person = m_AvailablePersons.Where(p => p.Equals(value)).First();}否则抛出新的 ArgumentOutOfRangeException("value");NotifyPropertyChanged("CurrentPerson");}

现在真正有趣的结果:

即使您将代码更改为使用 SelectedItem,它也可以正常绑定到列表,但仍然无法绑定到排序视图!

我向 Equals 方法添加了调试输出,即使找到了匹配项,它们也被忽略了:

public override bool Equals(object obj){如果(对象是人){Person other = obj as Person;if (other.Firstname == Firstname && other.Surname == Surname){Debug.WriteLine(string.Format("{0} == {1}", other.ToString(), this.ToString()));返回真;}别的{Debug.WriteLine(string.Format("{0} <> {1}", other.ToString(), this.ToString()));返回假;}}返回 base.Equals(obj);}

我的结论...

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

基本上你的 setter 更改是有效的,因为它保证了 CollectionViewSource 上的对象匹配,然后保证了 ComboBox 上的对象匹配.

测试代码

下面为那些想要玩的人提供完整的测试代码(抱歉代码隐藏,但这仅用于测试而不是 MVVM).

只需创建一个新的 Silverlight 4 应用程序并添加这些文件/更改:

PersonViewModel.cs

使用系统;使用 System.Collections.Generic;使用 System.Collections.ObjectModel;使用 System.ComponentModel;使用 System.Diagnostics;使用 System.Linq;命名空间 PersonTests{公共类 PersonViewModel : INotifyPropertyChanged{私人人 m_Person = null;私有只读 ObservableCollectionm_AvailablePersons =new ObservableCollection(new List{新人(迈克",史密斯"),新人(杰克",杰克逊"),新人(安妮",土豚"),});公共 ObservableCollection有空人员{得到 { 返回 m_AvailablePersons;}}公众人物 CurrentPerson{得到 { 返回 m_Person;}放{如果(m_Person != 值){m_Person = 值;NotifyPropertyChanged("CurrentPerson");}}//设置//这有效//{//如果 (m_AvailablePersons.Contains(value)) {//m_Person = m_AvailablePersons.Where(p => p.Equals(value)).First();//}//否则抛出新的 ArgumentOutOfRangeException("value");//NotifyPropertyChanged("CurrentPerson");//}}私有无效 NotifyPropertyChanged(字符串名称){if (PropertyChanged != null){PropertyChanged(this, new PropertyChangedEventArgs(name));}}公共事件 PropertyChangedEventHandler PropertyChanged;}公开课人{公共字符串名字{获取;放;}公共字符串姓氏 { 获取;放;}公共人(字符串名字,字符串姓氏){this.Firstname = 名字;this.Surname = 姓氏;}公共覆盖字符串 ToS​​tring(){返回名字+"+姓氏;}公共覆盖布尔等于(对象 obj){如果(对象是人){Person other = obj as Person;if (other.Firstname == Firstname && other.Surname == Surname){Debug.WriteLine(string.Format("{0} == {1}", other.ToString(), this.ToString()));返回真;}别的{Debug.WriteLine(string.Format("{0} <> {1}", other.ToString(), this.ToString()));返回假;}}返回 base.Equals(obj);}}}

MainPage.xaml

<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="{绑定可用人员}"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" C​​lick="button1_Click"/><Button Content="Select Anne Aardvark" Height="23" Name="button2" Click="button2_Click"/></StackPanel></用户控件>

MainPage.xaml.cs

使用 System.Windows;使用 System.Windows.Controls;命名空间 PersonTests{公共部分类 MainPage : UserControl{公共主页(){初始化组件();this.DataContext = new PersonViewModel();}private void button1_Click(对象发送者,RoutedEventArgs e){(this.DataContext as PersonViewModel).CurrentPerson = new Person("Mike", "Smith");}private void button2_Click(对象发送者,RoutedEventArgs e){(this.DataContext as PersonViewModel).CurrentPerson = new Person("Anne", "Aardvark");}}}

Lets say we got a simple VM class

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

It would be enough to successfully databind to a ComboBox for example like this:

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

Notice that Person has Equals overloaded and when I set CurrentPerson value in ViewModel it causes combobox current item to display new value.

Now lets say I want to add sorting capabilities to my view using CollectionViewSource

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

Now combobox items source binding will look like this:

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

And it will be indeed sorted (if we add more items its clearly seen).

However when we change CurrentPerson in VM now (before with clear binding without CollectionView it worked fine) this change isn't displayed in bound ComboBox.

I believe that after that in order to set CurrentItem from VM we have to somehow access the View (and we dont go to View from ViewModel in MVVM), and call MoveCurrentTo method to force View display currentItem change.

So by adding additional view capabilities (sorting ) we lost TwoWay binding to existing viewModel which I think isn't expected behaviour.

Is there a way to preserve TwoWay binding here ? Or maybe I did smth wrong.

EDIT: actually situation is more complicated then it may appear, when I rewrite CurrentPerson setter like this:

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

}

it works fine!

Its buggy behaviour, or is there an explanation? For some reasons even though Equals is overloaded it requires reference equality of person object.

I really don't understand why It needs reference equality so I am adding a bounty for someone who can explain why normal setter doesn't work, when Equal method is overloaded which can clearly be seen in "fixing" code that uses it

解决方案

There are 2 problems ganging up on you, but you have highlighted a real problem with using CollectionViewSource with a ComboBox. I am still looking for alternatives to fix this in a "better way", but your setter fix avoids the problem for good reason.

I have reproduced your example in full detail to confirm the problem and a theory about the cause.

ComboBox binding to CurrentPerson does not use the equals operator to find a match IF YOU USE SelectedValue INSTEAD OF SelectedItem. If you breakpoint your override bool Equals(object obj) you will see it is not hit when you change the selection.

By changing your setter to the following, you are finding a specific matching object, using your Equals operator, so a subsequent value compare of 2 objects will work.

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

}

Now the really interesting result:

Even if you change your code to use SelectedItem, it will work for a normal binding to the list but still fail for any binding to the sorted view!

I added debug output to the Equals method and even though matches were found, they were ignored:

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

My conclusion...

...is that behind the scenes the ComboBox is finding a match, but because of the presence of the CollectionViewSource between it and the raw data it is then ignoring the match and comparing objects instead (to decide which one was selected). From memory a CollectionViewSource manages its own current selected item, so if you do not get an exact object match it will never work using a CollectionViewSource with a ComboxBox.

Basically your setter change works because it guarantees an object match on the CollectionViewSource, which then guarantees an object match on the ComboBox.

Test code

The full test code is below for those that want to play (sorry about the code-behind hacks, but this was just for testing and not MVVM).

Just create a new Silverlight 4 application and add these files/changes:

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

        }
    }
}

这篇关于数据绑定到 ComboBox 中的 CollectionViewSource 时如何保留 CurrentItem 的双向绑定的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆