Multiple ItemsControl on single collection applies filter to all views at once

我的梦境 提交于 2020-06-22 03:35:31

问题


Prerequisites: .NET 4.5.1

I have three TreeView controls that display three filtered variants of single collection instance. When I try to apply a filter on Items collection of one of controls this filter propagates to other controls automatically which prevents me to use different filters on different controls.

Is there any way to achieve the same result without having to maintain three instances of collections at once?

An example that shows the problem follows below. First two ListViews are bound to the same collection instance directly. Third one is bound to that instance through CompositeCollection. And the fourth is bound to independent collection. When I press "Set Filter" button ItemsControl.Items.Filter property if first ListView is set to IsAllowedItem method of WTest window. After this second istView.Items.Filter property somehow points to the same method while third and fourth ListView returns null. Another effect is that though third ListView shows null filter its collection is still filtered as you can see if you run the example. This very strange effect arises from the behavior of ItemCollection class that when based on ItemsSource property of owner element acquires underlying CollectionView from some application-wide storage via CollectionViewSource.GetDefaultCollectionView method. I don't know the reason of this implementation but suspect suspect that it's performance.

Test window WTest.xaml:

<Window x:Class="Local.WTest"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:s="clr-namespace:System;assembly=mscorlib"
        xmlns:c="clr-namespace:System.Collections;assembly=mscorlib"
        xmlns:local="clr-namespace:Local"
        Name="_WTest" Title="WTest" Height="300" Width="600">
    <Window.Resources>
        <c:ArrayList x:Key="MyArray">
            <s:String>Letter A</s:String>
            <s:String>Letter B</s:String>
            <s:String>Letter C</s:String>
        </c:ArrayList>
        <CompositeCollection x:Key="MyCollection" >
            <CollectionContainer Collection="{StaticResource ResourceKey=MyArray}"/>
        </CompositeCollection>
        <c:ArrayList x:Key="AnotherArray">
            <s:String>Letter A</s:String>
            <s:String>Letter B</s:String>
            <s:String>Letter C</s:String>
        </c:ArrayList>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <TextBlock Grid.Row="0" Grid.Column="0" Name="FilterLabel1"/>
        <TextBlock Grid.Row="0" Grid.Column="1" Name="FilterLabel2"/>
        <TextBlock Grid.Row="0" Grid.Column="2" Name="FilterLabel3"/>
        <TextBlock Grid.Row="0" Grid.Column="3" Name="FilterLabel4"/>
        <ListView Grid.Row="1" Grid.Column="0" Name="View1" ItemsSource="{StaticResource ResourceKey=MyArray}"/>
        <ListView Grid.Row="1" Grid.Column="1" Name="View2" ItemsSource="{StaticResource ResourceKey=MyArray}"/>
        <ListView Grid.Row="1" Grid.Column="2" Name="View3" ItemsSource="{StaticResource ResourceKey=MyCollection}"/>
        <ListView Grid.Row="1" Grid.Column="3" Name="View4" ItemsSource="{StaticResource ResourceKey=AnotherArray}"/>
        <Button Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="4" Content="Set Filter" Click="OnSetFilterButtonClick"/>
    </Grid>
</Window>

Code behind WTest.xaml.cs

namespace Local
{
    using System.Windows;

    public partial class WTest : Window
    {
        public WTest()
        {
            InitializeComponent();
            UpdateFilterLabels();
        }

        private bool IsAllowedItem(object item)
        {
            return "Letter A" == (string)item;
        }

        private void OnSetFilterButtonClick(object sender, RoutedEventArgs e)
        {
            View1.Items.Filter = IsAllowedItem;
            UpdateFilterLabels();
        }

        private void UpdateFilterLabels()
        {
            FilterLabel1.Text = (null == View1.Items.Filter) ? "No Filter" : View1.Items.Filter.Method.Name;
            FilterLabel2.Text = (null == View2.Items.Filter) ? "No Filter" : View2.Items.Filter.Method.Name;
            FilterLabel3.Text = (null == View3.Items.Filter) ? "No Filter" : View3.Items.Filter.Method.Name;
            FilterLabel4.Text = (null == View4.Items.Filter) ? "No Filter" : View4.Items.Filter.Method.Name;
        }
    }
}

And result after "Set Filter" button is clicked: Example: result of clicking "Set Filter" button


回答1:


  1. Create CollectionViewSource as a Resource.

    <CollectionViewSource x:Key="CVSKey" Source="{DynamicResource MyArray}"/>
    
  2. Use this CollectionViewSource as your ItemsSource . Replace your View1 as :

    <!--<ListView Grid.Row="1" Grid.Column="0" Name="View1" ItemsSource="{DynamicResource ResourceKey=MyArray}"/>-->
    <ListView Grid.Row="1" Grid.Column="0" Name="View1" ItemsSource="{Binding Source={StaticResource ResourceKey=CVSKey}}"/>
    

Thats it, now everything will work as you want it to.

Additionally, now you can apply filtering to this CollectionViewSource instead of View1 :

((CollectionViewSource)this.Resources["CVSKey"]).Filter += List_Filter;

void List_Filter(object sender, FilterEventArgs e)
{
    e.Accepted = (e.Item.ToString() == "Letter A") ? true : false;
}

Create separate CollectionViewSource for separate ListBoxes to create separate views from same underlying collection.

Search google for CollectionViewSource.




回答2:


I found a simple solution that does not require creating a CollectionViewSource resource in XAML or a ListCollectionView in code for every collection that needs its own filter.

My solution is to use an ValueConverter that converts the ItemsSource source to a CollectionViewSource.View

ItemsSourceConverter:

[ValueConversion(typeof(IEnumerable), typeof(IEnumerable))]
public class ItemsSourceConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is IEnumerable itemsSource && itemsSource != null)
        {
            return new CollectionViewSource() { Source = itemsSource }.View;
        }
        else
        {
            throw new Exception($"Value must be an {nameof(IEnumerable)}");
        }
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return DependencyProperty.UnsetValue;
    }
}

XAML:

<Window ...>
    <Window.Resources>
        <local:ItemsSourceConverter x:Key="ItemsSourceConverter"/>
    </Window.Resources>
...
    <ItemsControl Name="View1", 
                  ItemsSource="{Binding Collection1, Converter={StaticResource ItemsSourceConverter}, Mode=OneWay}" />
    <ItemsControl Name="View2", 
                  ItemsSource="{Binding Collection3, Converter={StaticResource ItemsSourceConverter}, Mode=OneWay}" />
    <ItemsControl Name="View3", 
                  ItemsSource="{Binding Collection3, Converter={StaticResource ItemsSourceConverter}, Mode=OneWay}" />
</Window>

Code behind:

public partial class MainWindow : Window
{
    ObservableCollection<DataClass> Collection1 { get; private set; }
    ObservableCollection<DataClass> Collection2 { get; private set; }
    ObservableCollection<DataClass> Collection3 { get; private set; }

    public MainWindow()
    {
        InitializeComponent();
    }

    ...

    private void SetFilters()
    {
        View1.Filter = (item) =>
        {
            // Filter logic
        };

        View2.Filter = (item) =>
        {
            // Filter logic
        };

        View2.Filter = (item) =>
        {
            // Filter logic
        };
    }

    ...
}

MVVM ItemsControl with Filter binding

If we want to use the above solution with MVVM, we can create an attached property to bind the ItemsControl.Filter to a filter defined in the ViewModel.

Filter attached property:

public static class CollectionViewExtensions
{
    public static readonly DependencyProperty FilterProperty = DependencyProperty.RegisterAttached(
        "Filter",
        typeof(Predicate<object>),
        typeof(CollectionViewExtensions),
        new PropertyMetadata(default(Predicate<object>), OnFilterChanged));

    public static void SetFilter(ItemsControl element, Predicate<object> value)
    {
        element.SetValue(FilterProperty, value);
    }

    public static Predicate<object> GetFilter(ItemsControl element)
    {
        return (Predicate<object>)element.GetValue(FilterProperty);
    }

    private static void OnFilterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is ItemsControl itemsControl && itemsControl.Items.CanFilter)
        {
            if (e.OldValue is Predicate<object> oldPredicate)
            {
                itemsControl.Items.Filter -= oldPredicate;
            }

            if (e.NewValue is Predicate<object> newPredicate)
            {
                itemsControl.Items.Filter += newPredicate;
            }
        }
    }
}

Source: https://stackoverflow.com/a/39438710/10927863

XAML:

<Window ...>
    <Window.Resources>
        <local:ItemsSourceConverter x:Key="ItemsSourceConverter"/>
    </Window.Resources>
...
    <ItemsControl ItemsSource="{Binding Collection1, Converter={StaticResource ItemsSourceConverter}, Mode=OneWay}" 
                  local:CollectionViewExtensions.Filter="{Binding Filter1}"/>
    <ItemsControl ItemsSource="{Binding Collection3, Converter={StaticResource ItemsSourceConverter}, Mode=OneWay}" 
                  local:CollectionViewExtensions.Filter="{Binding Filter2}"/>
    <ItemsControl ItemsSource="{Binding Collection3, Converter={StaticResource ItemsSourceConverter}, Mode=OneWay}" 
                  local:CollectionViewExtensions.Filter="{Binding Filter3}"/>
</Window>

ViewModel:

public class ViewModel
{
    ObservableCollection<DataClass> Collection1 { get; private set; }
    ObservableCollection<DataClass> Collection2 { get; private set; }
    ObservableCollection<DataClass> Collection3 { get; private set; }

    public Predicate<object> Filter1 { get; private set; }
    public Predicate<object> Filter2 { get; private set; }
    public Predicate<object> Filter3 { get; private set; }

    ...

    private void SetFilters()
    {
        Filter1 = (item) =>
        {
            // Filter logic
        };

        Filter2 = (item) =>
        {
            // Filter logic
        };

        Filter3 = (item) =>
        {
            // Filter logic
        };
    }

    ...
}



回答3:


Change the OnSetFilterButtonClick Method as below

private void OnSetFilterButtonClick(object sender, RoutedEventArgs e)
{ 
    //Create a new listview by the ItemsSource,Apply Filter to the new listview
    ListCollectionView listView = new ListCollectionView(View1.ItemsSource as IList); 
    listView.Filter = IsAllowedItem;
    View1.ItemsSource = listView;
    UpdateFilterLabels();
}


来源:https://stackoverflow.com/questions/38542781/multiple-itemscontrol-on-single-collection-applies-filter-to-all-views-at-once

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!