【问题标题】:WPF: filtering ItemCollection of a ComboBox also filters other ComboBoxes bound to the same ItemsSource [duplicate]WPF:过滤 ComboBox 的 ItemCollection 还过滤绑定到同一 ItemsSource 的其他 ComboBox [重复]
【发布时间】:2020-02-02 22:00:18
【问题描述】:

给定一个“客户”实体:

public class CustomerEntity: EntityBase
{
    public Dictionary<int, string> ClientsOfCustomer = new Dictionary<int, string>();

    public CustomerEntity() 
    {
        // Load ClientsOfCustomer...
    }
}

以及两个或更多 WPF 组合框,它们的 ItemsSourceProperty 绑定到同一源(例如,上述“客户”实体的属性):

var comboBox1 = new ComboBox();
var comboBox2 = new ComboBox();
comboBox1.SetBinding(ItemsControl.ItemsSourceProperty,
    new Binding(itemsSourceProperty) { ElementName = "ClientsOfCustomer" });
comboBox2.SetBinding(ItemsControl.ItemsSourceProperty,
    new Binding(itemsSourceProperty) { ElementName = "ClientsOfCustomer" });

以上是在我们真正拥有底层对象的实例之前完成的。这会在应用程序中稍后发生:

var customer = new CustomerEntity();
parentPageOfComboboxes.DataContext = customer;

如果我们随后过滤ItemCollectionComboBox

comboBox1.Items.Filter = i => ((KeyValuePair<int, string>)i).Value.StartsWith("a");

这最终也会过滤另一个ComboBox

我知道这是因为ComboBox 是一个Selector,它是一个ItemsControl,并且ItemsControlItemsSource DependencyProperty 更改时调用ItemCollection.SetItemsSource。然后ItemCollection 使用CollectionViewSource.GetDefaultCollectionView( _itemsSource, ModelParent, GetSourceItem) 从缓存中获取与给定_itemsSource 关联的默认CollectionView。因此,如果您在一个ComboBox 的底层ItemCollection 上设置过滤器,您实际上是在与ComboBox 绑定的ItemsSource 关联的缓存和共享CollectionView 上设置它。我猜 WPF 的人们没想到有人会这样做——不幸的是。但是我能做些什么呢?

我在 SO 上发现了两个类似的问题,但没有任何真正的答案:“Filtered Combobox ItemsSource binding issue”(似乎是关于自定义“FilteredComboBox”)和“Wpf ListBoxes' ItemsSource strange behaviour(涉及没有数据绑定的列表框)。

我还实现了一个相当丑陋的解决方案,我将在下面给出答案。但是,必须有更好的方法。

更新/澄清:我在最初的问题中没有清楚地表达自己。如果我们可以访问我们在数据绑定时绑定到的实体的实际实例,那么这个问题的答案是微不足道的。但是,我有一个复杂的应用程序,它可以从元数据“即时”生成表单(可以来自 C# 属性,也可以来自存储在数据库中的元数据,没关系)。在 SO 上提出的其他类似问题中,实际绑定源(数据上下文)始终可用,因此只需执行类似 Items = new ListCollectionView(...); 的操作(在代码中或在 XAML 中)是可能的。但是,如果实际的DataContext 只是基于元数据设置在应用程序生命周期的某个更远的地方呢?除了循环所有生成的 UI 元素并追溯更改绑定源之外,还有什么解决方案?我希望这个澄清是可以理解的。

【问题讨论】:

标签: c# .net wpf xaml combobox


【解决方案1】:

一个丑陋的解决方案

我创建了自己的MyComboBox,它继承自System.Windows.Controls.ComboBox。在MyComboBox 中,我覆盖ItemsSourceProperty as described in the docs 的元数据,这样每当ItemsSource 更改时,我实际上绑定到一个新的ListCollectionView

public partial class MyComboBox : ComboBox
{
    static MyComboBox()
    {
        ItemsSourceProperty.OverrideMetadata(typeof(MyComboBox),
            new FrameworkPropertyMetadata((IEnumerable)null, new PropertyChangedCallback(OnItemsSourceChanged)));
    }

    private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ItemsControl ic = (ItemsControl)d;
        IEnumerable oldValue = (IEnumerable)e.OldValue;
        IEnumerable newValue = (IEnumerable)e.NewValue;

        // We get into infinite recursion land without the following condition:
        if (newValue.GetType() == typeof(Dictionary<int, string>))
        {
            var cvs = new CollectionViewSource() { Source = ((Dictionary<int, string>)newValue) };
            ic.ItemsSource = cvs.View;
        }
    }

    ...
}

请注意,if (newValue.GetType() == typeof(Dictionary&lt;int, string&gt;)) 条件是必需的,否则您将进入无限递归循环(MyComboBox 设置 ItemsControl.ItemsSource 会更改 ItemsSourceProperty,然后触发 ic.OnItemsSourceChanged(oldValue, newValue) 导致回到 MyComboBox.OnItemsSourceChanged 等等开。

另一个警告:简单地执行ic.ItemsSource = new ListCollectionView(((Dictionary&lt;int, string&gt;)newValue).ToList()); 似乎可行,只是对底层ObservableCollection 的任何更改都不会更新绑定的ComboBox。所以似乎有必要创建一个新的CollectionViewSource,设置Source 并绑定到上面的视图。这也是official docs for CollectionView中推荐的:

您不应在代码中创建此类的对象。创建一个 仅实现 IEnumerable 的集合的集合视图, 创建一个 CollectionViewSource 对象,将您的集合添加到 Source 属性,并从 View 属性中获取集合视图。

所以上面的方法有效,但它很丑陋。真的没有更好的办法吗?

【讨论】:

    【解决方案2】:

    您的观察是正确的。每个绑定到同一源集合的ItemsControl 共享此集合的公共默认ICollectionView

    解决方案是为每个ComboBox 创建ICollectionView 的专用实例。您可以使用 CollectionViewSource 在 XAML 中定义它们,如下例所示。如果过滤器由Button 触发,您可以使用命令将此Button 绑定到视图模型中定义的过滤谓词,将CollectionViewSource 传递为CommandParameter

    否则,将ICollectionView 定义为视图模型中的属性并绑定到它们。您现在可以直接设置过滤谓词,而无需使用视图中的触发器(命令)。

    ViewModel.cs

    public ObservableCollection<string> Data source { get; set; }
    
    ICommand ApplyFilterCommand => new RelayCommand(FilterCollectionView);
    
    private void FilterCollectionView(object param)
    {  
        CollectionView collectionView = (param as CollectionViewSource).View;
        collectionView.Filter = item => item.StartsWith("a");
        collectionView.Refresh();
    }  
    

    MainWindow.xaml

    <Window>
        <Window.DataContext>
            <ViewModel />
        <Window.DataContext>
        <Window.Resources>
            <CollectionViewSource x:Key="FirstCollectionSource" 
                                  Source="{Binding DataSource}" />
            <CollectionViewSource x:Key="SecondCollectionSource" 
                                  Source="{Binding DataSource}" />
        <Window.Resources>
    
        <StackPanel>
            <ComboBox ItemsSource="{Binding Source={StaticResource FirstCollectionSource}}" />
            <ComboBox ItemsSource="{Binding Source={StaticResource SecondCollectionSource}}" />
    
            <!-- Filter the second ComboBox -->
            <Button Command="{Binding ApplyFilterCommand}"
                    CommandParameter="{StaticResource SecondCollectionSource}" />
        </StackPanel>
    <Window>
    

    【讨论】:

    • 我的道歉,我原来的问题不够清楚。我做了一些澄清。所以,基本上,当组合框被创建时,我无法访问实际的绑定源(数据上下文),所以我不能像你上面描述的那样做:-(
    猜你喜欢
    • 1970-01-01
    • 2015-04-07
    • 2011-08-23
    • 1970-01-01
    • 1970-01-01
    • 2014-02-28
    • 2020-01-30
    • 2011-12-06
    • 1970-01-01
    相关资源
    最近更新 更多