15

假设我们有一个简单的 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}" />    

它确实会被排序(如果我们添加更多项目,它会清晰可见)。

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

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

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

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

编辑:当我像这样重写 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,当方法重载时,这可以在使用它的“修复”代码中清楚地看到

4

4 回答 4

11

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

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

如果您使用 SelectedValue而不是 SelectedItem ,则 ComboBox 绑定到 CurrentPerson 不使用等号运算符来查找匹配项。如果您设置断点,您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 管理自己的当前选定项目,因此如果您没有获得精确的对象匹配,它将永远无法使用 CollectionViewSource 和 ComboxBox

基本上,您的 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);
        }
    }
}

主页.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");

        }
    }
}
于 2011-06-24T11:45:08.713 回答
1

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

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

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

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

公共 ICollectionView AvailablePersonsView {get;私人集;}

在 ctor 中:

AvailablePersonsView = CollectionViewSource.GetDefaultView(AvailablePersons)

于 2011-06-27T09:04:01.707 回答
0

TwoWay 绑定按其应有的方式工作,但ComboBox不会在您设置SelectedItemSelectedIndex通过代码时在 UI 上自行更新。如果您想要此功能,只需扩展ComboBox并收听SelectionChanged继承自,Selector或者如果您只想设置初始选择,请在Loaded.

于 2011-06-10T11:51:44.200 回答
0

我强烈推荐使用 Microsoft 的 Kyle McClellan 的 ComboBoxExtensions,可在此处找到。

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

基本上,解决方案主要是不使用 CollectionViewSource 的组合框。您可以对服务器端查询进行排序。

于 2012-05-21T20:00:39.797 回答