【问题标题】:How to preserve TwoWay binding of CurrentItem when databinding to CollectionViewSource in ComboBox数据绑定到 ComboBox 中的 CollectionViewSource 时如何保留 CurrentItem 的双向绑定
【发布时间】:2011-09-12 10:23:29
【问题描述】:

假设我们有一个简单的 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 更改。

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

有没有办法在这里保留双向绑定?或者我做错了什么。

编辑:当我像这样重写 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 被重载,它也需要人对象的引用相等

我真的不明白为什么它需要引用相等,所以我添加一个 赏金 来解释为什么正常的 setter 不起作用,当 Equal 方法重载时,可以清楚地在使用它的“修复”代码中可以看到

【问题讨论】:

  • +1 很好地发现了与 ComboxBoxes 一起使用的 CollectionViewSources 中的一个真正缺陷!

标签: c# silverlight silverlight-4.0 collectionviewsource


【解决方案1】:

您遇到了 2 个问题,但您强调了将 CollectionViewSource 与 ComboBox 结合使用的真正问题。我仍在寻找以“更好的方式”解决此问题的替代方法,但您的 setter 修复程序有充分的理由避免了该问题。

我已详细复制了您的示例,以确认问题和有关原因的理论。

ComboBox 绑定到 CurrentPerson 不使用等号运算符来查找匹配如果您使用 SelectedValue 而不是 SelectedItem。如果您对override bool Equals(object obj) 进行断点,您将看到更改选择时它没有被命中。

通过将您的设置器更改为以下内容,您可以使用 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");

        }
    }
}

【讨论】:

  • 我理解正确吗:如果我们使用 CurrentItem,collectionviewsource 在没有特殊引用的情况下会表现良好 equals setter?
  • @Valentin Kuzub:只有将它设置为集合中的一个对象(而不是另一个看起来相同的对象)时,它才会起作用(没有你的特殊设置器)。可以把它想象成比较 C++ 中的对象指针。
  • 您提供了双向绑定,但仅提供了一个 getter。二传手长什么样子?
  • 重写 EQUAL 让我有一天时间调试为什么 listview 的 SelectedItem 在更新上下文后不会改变:((
【解决方案2】:

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

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

我已经使用 collectionview 进行排序和分组没有问题 - 你可以很好地与 ui 解耦。

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

public ICollectionView AvailablePersonsView {get;私有集;}

在 ctor 中:

AvailablePersonsView = CollectionViewSource.GetDefaultView(AvailablePersons)

【讨论】:

    【解决方案3】:

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

    【讨论】:

    • 对不起,也许我不清楚,我没有从 VM 访问 combobox.SelectedItem,我正在将 VM 的 CurrentPerson 属性设置为我想在 UI 中显示的内容,我将编辑有问题的这部分.这个新的 CurrentPerson 的行为类似于它在 OneWayToSourceMode 中的绑定,从组合框中选择值会更改 VM CurrentPerson,但在 VM 中设置 CurrentPerson 不会更改组合框
    【解决方案4】:

    我强烈推荐使用 Microsoft 的 Kyle McClellan 的 ComboBoxExtensions,发现 here

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

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

    【讨论】:

      猜你喜欢
      • 2011-05-03
      • 2020-05-23
      • 2012-07-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多