问题
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:
Create
CollectionViewSource
as aResource
.<CollectionViewSource x:Key="CVSKey" Source="{DynamicResource MyArray}"/>
Use this
CollectionViewSource
as yourItemsSource
. 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