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

时间:2011-06-10 11:20:02

标签: c# silverlight silverlight-4.0 collectionviewsource

假设我们有一个简单的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重载,当我在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>

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

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

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

但是,当我们现在在VM中更改CurrentPerson时(在没有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");

}
  

它有效fine

它有缺陷的行为,还是有解释?出于某些原因,即使Equals超载,也需要人物对象的引用相等

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

4 个答案:

答案 0 :(得分:9)

你有两个问题,但你突出了使用CollectionViewSource和ComboBox的一个真正问题。我仍在寻找替代方案以“更好的方式”修复此问题,但您的 setter 修复程序有充分的理由避免了这个问题。

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

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

通过将setter更改为以下内容,您将使用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管理自己当前选择的项目 ,因此如果您没有获得精确的对象匹配,则使用带有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);
        }
    }
}

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 :(得分:1)

您是否考虑过使用CollectionView并在组合框上设置IsSynchronizedWithCurrentItem?

这就是我要做的 - 不是拥有你的CurrentPerson属性,而是在collectionView.CurrentItem上有选定的人,并且在collectionview上有关于currentitem的组合框。

我已经使用了collectionview进行排序和分组而没有任何问题 - 你可以从ui中获得很好的解耦。

我会将collectionview移动到代码并绑定到那里

public ICollectionView AvailablePersonsView {get; private set;}

在ctor:

AvailablePersonsView = CollectionViewSource.GetDefaultView(AvailablePersons)

答案 2 :(得分:0)

TwoWay绑定可以正常工作,但是当您从代码中设置ComboBoxSelectedItem时,SelectedIndex不会在UI上自行更新。如果您需要此功能,只需展开ComboBox并聆听从SelectionChanged继承的Selector,或者如果您只想设置初始选择,请在Loaded上进行。

答案 3 :(得分:0)

我强烈建议微软的Kyle McClellan使用ComboBoxExtensions,找到here

您可以在XAML中为ComboBox声明一个数据源 - 它在异步模式下更加灵活和可用。

基本上,解决方案主要是不要将CollectionViewSource用于ComboBoxes。您可以在服务器端查询进行排序。