如何保存在数据绑定组合框到CollectionViewSource时CURRENTITEM的双向绑定 [英] How to preserve TwoWay binding of CurrentItem when databinding to CollectionViewSource in ComboBox

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

问题描述

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

 公共类PersonViewModel:可观察
{
私人人m_Person =新的Person(迈克,史密斯);

私人只读的ObservableCollection<&人GT; m_AvailablePersons =
新的ObservableCollection<人>(新的List<人> {
新的人(迈克,史密斯),
新的人(杰克,禅师),
});

公众的ObservableCollection<&人GT; AvailablePersons
{
{返回m_AvailablePersons; }
}

公众人物CurrentPerson
{
{返回m_Person; }

{
m_Person =价值;
NotifyPropertyChanged(CurrentPerson);
}
}
}



这将足以成功数据绑定像这样的例子组合框:

 <组合框的ItemsSource ={结合AvailablePersons}
的SelectedValue = {结合路径= CurrentPerson,模式=双向}/>



注意那个人等于超载时我设置CurrentPerson值视图模型它会导致组合框当前项,以显示新的值。



现在可以说,我要添加使用排序功能,以我的看法 CollectionViewSource

 < UserControl.Resources> 
< CollectionViewSource X:键=PersonsViewSource来源={结合AvailablePersons}>
< CollectionViewSource.SortDescriptions>
< SCM:SortDescription属性名=姓氏方向=升序/>
< /CollectionViewSource.SortDescriptions>
< / CollectionViewSource>
< /UserControl.Resources>

现在组合框的项目源绑定将是这样的:

 <组合框的ItemsSource ={绑定源= {StaticResource的PersonsViewSource}}
的SelectedValue ={绑定路径= CurrentPerson,模式=双向}/>

和它将确实排序(如果我们增加更多的项目其清楚地看到)。



然而,当我们改变 CurrentPerson 在VM现在(与前没有明确的CollectionView结合它工作得很好)本次变动不显示绑定组合框。



我相信,经过,为了设置与VM CURRENTITEM我们必须以某种方式访问​​View(我们不去从视图模型在MVVM查看)并调用 MoveCurrentTo 方法强制视图显示CURRENTITEM变化。



因此​​,通过增加额外的视图的功能(排序)我们失去双向绑定到现有的视图模型,我认为是不期望的行为。



有没有办法保持双向绑定在这里?或者,也许我确实不便错



编辑:实际情况比较复杂那么它可能会出现,当我重写CurrentPerson setter方法​​是这样的:

  {
如果(m_AvailablePersons.Contains(值)){
m_Person = m_AvailablePersons> 。凡(p => p.Equals(值))第一()。
}
,否则抛出新ArgumentOutOfRangeException(值);
NotifyPropertyChanged(CurrentPerson」);

}




它的工作原理罚款




其错误行为,或者是有一个解释?因为某些原因,即使等于超载它需要的引用相等人员对象。



我真的不明白,为什么它需要引用相等,所以我加入了赏金人谁可以解释为什么正常的二传手不工作,当等于方法被重载,可以清楚地在使用固定的代码可以看出它


解决方案

有2个问题勾搭你,而你却凸显了真正的问题,使用CollectionViewSource一个组合框。我仍然在寻找替代在一个更好的方式解决这一问题,但你的二传手的修复避免了很好的理由的问题。



我已经转载了全部细节的例子来确认问题和对事业的理论。



组合框结合CurrentPerson不使用等号运算符找到匹配的如果您使用的SelectedValue的SelectedItem INSTEAD OF。如果断点你的覆盖布尔等于(obj对象)你会看到它时,你改变选择是打不到。



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

 设置
{
如果(m_AvailablePersons.Contains(值)){
m_Person = m_AvailablePersons.Where(p => p.Equals(值))。第一();
}
,否则抛出新ArgumentOutOfRangeException(值);
NotifyPropertyChanged(CurrentPerson);

}



现在真的很有趣的结果:



即使你改变你的代码中使用的SelectedItem,它会为一个正常的绑定工作,列表中,但仍无法为任何约束力的排序视图!



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

 公共覆盖布尔等于(obj对象)
{
如果(obj是人)
{
等人的obj =的人;
如果(other.Firstname ==名字和放大器;&安培; other.Surname ==姓)
{
的Debug.WriteLine(的String.Format({0} == {1} ,other.ToString(),this.ToString()));
返回真;
}
,否则
{
的Debug.WriteLine(的String.Format({0}<> {1},other.ToString(),this.ToString( )));
返回FALSE;
}
}
返回base.Equals(OBJ);
}



我的结论...



...是幕后的组合框是找到一个匹配,但由于CollectionViewSource它与原始数据之间存在的是则忽略匹配,而是比较对象(决定选择哪一个)。从内存中的CollectionViewSource管理自己的当前选择的项目, ,所以如果你没有得到一个确切的对象匹配它永远不会使用CollectionViewSource与ComboxBox工作



基本上,你的二传手变化的作品,因为它保证的CollectionViewSource,这样就保证对组合框的对象匹配对象匹配。



测试代码



完整的测试代码如下对于那些想玩(抱歉隐藏代码的黑客,但是这只是用于测试,而不是MVVM)。



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



PersonViewModel.cs



 使用系统; 
使用System.Collections.Generic;
使用System.Collections.ObjectModel;
使用System.ComponentModel;使用System.Diagnostics程序
;
使用System.Linq的;
命名空间PersonTests
{
公共类PersonViewModel:INotifyPropertyChanged的
{
私募人士m_Person = NULL;

私人只读的ObservableCollection<&人GT; m_AvailablePersons =
新的ObservableCollection<人>(新的List<人> {
新的人(迈克,史密斯),
新的人(杰克,禅师),
新的Person(安妮,土豚),
});

公众的ObservableCollection<&人GT; AvailablePersons
{
{返回m_AvailablePersons; }
}

公众人物CurrentPerson
{
{返回m_Person; }

{
如果(!m_Person =值)
{
m_Person =价值;
NotifyPropertyChanged(CurrentPerson);
}
}

// //设置此工程
// {
//如果(m_AvailablePersons.Contains(值)){
// m_Person = m_AvailablePersons.Where(p => p.Equals(值))。首先();
//}
//否则抛出新ArgumentOutOfRangeException(值);
// NotifyPropertyChanged(CurrentPerson);
//}
}

私人无效NotifyPropertyChanged(字符串名称)
{
如果(的PropertyChanged!= NULL)
{
的PropertyChanged(这一点,新PropertyChangedEventArgs(名));
}
}

公共事件PropertyChangedEventHandler的PropertyChanged;
}

公共类Person
{
公共字符串名字{获得;组; }
公共字符串姓氏{搞定;组; }

公众人物(字符串名字,姓字符串)
{
this.Firstname =名字;
this.Surname =姓;
}

公共重写字符串的ToString()
{
返回姓++姓;
}

公众覆盖布尔等于(obj对象)
{
如果(obj是人)
{
的人除外= OBJ时人;
如果(other.Firstname ==名字和放大器;&安培; other.Surname ==姓)
{
的Debug.WriteLine(的String.Format({0} == {1} ,other.ToString(),this.ToString()));
返回真;
}
,否则
{
的Debug.WriteLine(的String.Format({0}<> {1},other.ToString(),this.ToString( )));
返回FALSE;
}
}
返回base.Equals(OBJ);
}
}
}



MainPage.xaml中



 <用户控件X:类=PersonTests.MainPage
的xmlns =htt​​p://schemas.microsoft.com/ WinFX的/ 2006 / XAML /演示
的xmlns:X =http://schemas.microsoft.com/winfx/2006/xaml
的xmlns:D =http://schemas.microsoft。 COM /表达/混合/ 2008
的xmlns:MC =http://schemas.openxmlformats.org/markup-compatibility/2006的xmlns:SCM =CLR的命名空间:System.ComponentModel程序;装配=系统。 Windows的MC:可忽略=D
D:DesignHeight =300D:DesignWidth =400>
< UserControl.Resources>
< CollectionViewSource X:键=PersonsViewSource来源={结合AvailablePersons}>
< CollectionViewSource.SortDescriptions>
< SCM:SortDescription属性名=姓氏方向=升序/>
< /CollectionViewSource.SortDescriptions>
< / CollectionViewSource>
< /UserControl.Resources>
< StackPanel的X:名称=LayoutRoot背景=LightBlueWIDTH =150>
<! - <组合框的ItemsSource ={结合AvailablePersons}
的SelectedItem ={绑定路径= CurrentPerson,模式=双向}/> - >
<组合框的ItemsSource ={绑定源= {StaticResource的PersonsViewSource}}
的SelectedItem ={绑定路径= CurrentPerson,模式=双向}/>
<按钮内容=选择迈克·史密斯HEIGHT =23NAME =Button1的点击=的button1_Click/>
<按钮内容=选择安土豚HEIGHT =23NAME =按钮2点击=button2_Click/>
< / StackPanel的>
< /用户控件>



MainPage.xaml.cs中



 使用System.Windows;使用System.Windows.Controls的
;

命名空间PersonTests
{
公共部分类的MainPage:用户控件
{
公众的MainPage()
{
的InitializeComponent() ;
this.DataContext =新PersonViewModel();
}

私人无效的button1_Click(对象发件人,RoutedEventArgs E)
{
(this.DataContext为PersonViewModel).CurrentPerson =新的Person(迈克,史密斯);
}

私人无效button2_Click(对象发件人,RoutedEventArgs E)
{
(this.DataContext为PersonViewModel).CurrentPerson =新的Person(安妮,土豚);

}
}
}


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

        }
    }
}

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

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