在CollectionViewSource上触发filter
我正在使用MVVM模式在WPF桌面应用程序上工作。
我想基于在TextBox
键入的文本筛选ListView
的一些项目。 我希望ListView
项目被过滤,因为我改变了文本。
我想知道如何在filter文本更改时触发filter。
ListView
绑定到一个CollectionViewSource
,绑定到ViewModel上的ObservableCollection
。 filter文本的TextBox
绑定到ViewModel上的一个string,它应该是UpdateSourceTrigger=PropertyChanged
。
<CollectionViewSource x:Key="ProjectsCollection" Source="{Binding Path=AllProjects}" Filter="CollectionViewSource_Filter" /> <TextBox Text="{Binding Path=FilterText, UpdateSourceTrigger=PropertyChanged}" /> <ListView DataContext="{StaticResource ProjectsCollection}" ItemsSource="{Binding}" />
Filter="CollectionViewSource_Filter"
链接到后面的代码中的事件处理程序,它只是在ViewModel上调用filter方法。
当FilterText的值发生变化时进行筛选 – FilterText属性的setter调用一个FilterList方法,该方法遍历ViewModel中的ObservableCollection
,并在每个ViewModel项上设置一个boolean
FilteredOut属性。
我知道FilteredOut属性在filter文本更改时更新,但List不刷新。 CollectionViewSource
filter事件只有在重新加载UserControl的时候才会被触发。
我已经尝试更新filter信息后调用OnPropertyChanged("AllProjects")
,但它并没有解决我的问题。 (“AllProjects”是我的ViewModel的CollectionViewSource
绑定的ObservableCollection
属性。)
当FilterText TextBox
的值发生变化时,如何让CollectionViewSource
重新进行自我刷新?
非常感谢
不要在您的视图中创build一个CollectionViewSource
。 相反,在您的视图模型中创build一个types为ICollectionView
的属性并将ListView.ItemsSource
绑定到它。
完成此操作后,可以将逻辑放入FilterText
属性的setter中,以便在用户更改ICollectionView
时调用Refresh()
。
您会发现这也简化了sorting问题:您可以将sorting逻辑构build到视图模型中,然后显示视图可以使用的命令。
编辑
下面是一个使用MVVMdynamicsorting和过滤集合视图的简单演示。 这个演示没有实现FilterText
,但是一旦你理解了它是如何工作的,你不应该有任何困难实现一个FilterText
属性和一个使用该属性的谓词,而不是现在使用的硬编码filter。
(还要注意,这里的视图模型类没有实现属性更改通知,只是为了保持代码简单:因为这个演示中没有任何内容实际上改变属性值,所以不需要属性改变通知。
首先为你的物品上课:
public class ItemViewModel { public string Name { get; set; } public int Age { get; set; } }
现在,应用程序的视图模型。 有三件事情在这里:首先,它创build并填充自己的ICollectionView
; 第二,它公开了一个ApplicationCommand
(见下文),该视图将用来执行sorting和过滤命令,最后,它实现了一个Execute
方法来对视图进行sorting或过滤:
public class ApplicationViewModel { public ApplicationViewModel() { Items.Add(new ItemViewModel { Name = "John", Age = 18} ); Items.Add(new ItemViewModel { Name = "Mary", Age = 30} ); Items.Add(new ItemViewModel { Name = "Richard", Age = 28 } ); Items.Add(new ItemViewModel { Name = "Elizabeth", Age = 45 }); Items.Add(new ItemViewModel { Name = "Patrick", Age = 6 }); Items.Add(new ItemViewModel { Name = "Philip", Age = 11 }); ItemsView = CollectionViewSource.GetDefaultView(Items); } public ApplicationCommand ApplicationCommand { get { return new ApplicationCommand(this); } } private ObservableCollection<ItemViewModel> Items = new ObservableCollection<ItemViewModel>(); public ICollectionView ItemsView { get; set; } public void ExecuteCommand(string command) { ListCollectionView list = (ListCollectionView) ItemsView; switch (command) { case "SortByName": list.CustomSort = new ItemSorter("Name") ; return; case "SortByAge": list.CustomSort = new ItemSorter("Age"); return; case "ApplyFilter": list.Filter = new Predicate<object>(x => ((ItemViewModel)x).Age > 21); return; case "RemoveFilter": list.Filter = null; return; default: return; } } }
分类的吮吸; 你需要实现一个IComparer
:
public class ItemSorter : IComparer { private string PropertyName { get; set; } public ItemSorter(string propertyName) { PropertyName = propertyName; } public int Compare(object x, object y) { ItemViewModel ix = (ItemViewModel) x; ItemViewModel iy = (ItemViewModel) y; switch(PropertyName) { case "Name": return string.Compare(ix.Name, iy.Name); case "Age": if (ix.Age > iy.Age) return 1; if (iy.Age > ix.Age) return -1; return 0; default: throw new InvalidOperationException("Cannot sort by " + PropertyName); } } }
为了在视图模型中触发Execute
方法,它使用了一个ApplicationCommand
类,它是一个ICommand
的简单实现,它将视图中的button上的CommandParameter
路由到视图模型的Execute
方法。 我这样实现它,因为我不想在应用程序视图模型中创build一堆RelayCommand
属性,而且我希望在一个方法中保留所有的sorting/过滤,以便很容易地看到它是如何完成的。
public class ApplicationCommand : ICommand { private ApplicationViewModel _ApplicationViewModel; public ApplicationCommand(ApplicationViewModel avm) { _ApplicationViewModel = avm; } public void Execute(object parameter) { _ApplicationViewModel.ExecuteCommand(parameter.ToString()); } public bool CanExecute(object parameter) { return true; } public event EventHandler CanExecuteChanged; }
最后,这里是应用程序的MainWindow
:
<Window x:Class="CollectionViewDemo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:CollectionViewDemo="clr-namespace:CollectionViewDemo" Title="MainWindow" Height="350" Width="525"> <Window.DataContext> <CollectionViewDemo:ApplicationViewModel /> </Window.DataContext> <DockPanel> <ListView ItemsSource="{Binding ItemsView}"> <ListView.View> <GridView> <GridViewColumn DisplayMemberBinding="{Binding Name}" Header="Name" /> <GridViewColumn DisplayMemberBinding="{Binding Age}" Header="Age"/> </GridView> </ListView.View> </ListView> <StackPanel DockPanel.Dock="Right"> <Button Command="{Binding ApplicationCommand}" CommandParameter="SortByName">Sort by name</Button> <Button Command="{Binding ApplicationCommand}" CommandParameter="SortByAge">Sort by age</Button> <Button Command="{Binding ApplicationCommand}" CommandParameter="ApplyFilter">Apply filter</Button> <Button Command="{Binding ApplicationCommand}" CommandParameter="RemoveFilter">Remove filter</Button> </StackPanel> </DockPanel> </Window>
如今,您通常不需要明确触发刷新。 CollectionViewSource
实现了ICollectionViewLiveShaping
,如果IsLiveFilteringRequested
为true,它会根据LiveFilteringProperties
集合中的字段自动更新。
XAML中的示例:
<CollectionViewSource Source="{Binding Items}" Filter="FilterPredicateFunction" IsLiveFilteringRequested="True"> <CollectionViewSource.LiveFilteringProperties> <system:String>FilteredProperty1</system:String> <system:String>FilteredProperty2</system:String> </CollectionViewSource.LiveFilteringProperties> </CollectionViewSource>
也许你已经在你的问题中简化了视图,但是按照书面的说法,你并不需要一个CollectionViewSource–你可以直接绑定到ViewModel中的一个过滤列表(mItemsToFilter是被过滤的集合,可能是“AllProjects”你的例子):
public ReadOnlyObservableCollection<ItemsToFilter> AllFilteredItems { get { if (String.IsNullOrEmpty(mFilterText)) return new ReadOnlyObservableCollection<ItemsToFilter>(mItemsToFilter); var filtered = mItemsToFilter.Where(item => item.Text.Contains(mFilterText)); return new ReadOnlyObservableCollection<ItemsToFilter>( new ObservableCollection<ItemsToFilter>(filtered)); } } public string FilterText { get { return mFilterText; } set { mFilterText = value; if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs("FilterText")); PropertyChanged(this, new PropertyChangedEventArgs("AllFilteredItems")); } } }
您的观点将只是:
<TextBox Text="{Binding Path=FilterText,UpdateSourceTrigger=PropertyChanged}" /> <ListView ItemsSource="{Binding AllFilteredItems}" />
一些快速注释:
-
这消除了后面代码中的事件
-
它也消除了“FilterOut”属性,这是一个人造的,仅GUI的属性,从而真正打破了MVVM。 除非你打算序列化这个,否则我不会想要在我的ViewModel中,当然也不在我的Model中。
-
在我的例子中,我使用了“Filter In”而不是“Filter Out”。 对我来说(在大多数情况下),我正在使用的filter是我想看到的东西,这似乎更符合逻辑。 如果你真的想过滤出来,只是否定Contains子句(即item =>!Item.Text.Contains(…))。
-
你可能有更集中的方式来做你的视图模型集。 重要的是要记住,当你改变FilterText的时候,你还需要通知你的AllFilteredItems集合。 我在这里做了内联,但是你也可以处理PropertyChanged事件,并在e.PropertyName是FilterText时调用PropertyChanged。
请让我知道,如果你需要任何澄清。
如果我明白你在问什么:
在您的FilterText
属性的设置部分,只需调用Refresh()
到您的CollectionView
。
我刚刚发现了一个更加优雅的解决scheme。 而不是创build一个ICollectionView
在您的ViewModel(如接受的答案build议),并设置您的绑定
ItemsSource={Binding Path=YourCollectionViewSourceProperty}
更好的方法是在ViewModel中创build一个CollectionViewSource
属性。 然后如下绑定你的ItemsSource
ItemsSource={Binding Path=YourCollectionViewSourceProperty.View}
注意添加.View这样,只要有一个对CollectionViewSource
的改变, ItemsSource
绑定仍然会被通知,并且你不需要在ICollectionView
上手动调用Refresh()
注意:我无法确定为什么会这样。 如果直接绑定到CollectionViewSource
属性,则绑定失败。 但是,如果您在XAML文件的Resources
元素中定义CollectionViewSource
并直接绑定到资源键,则绑定工作正常。 我唯一能猜到的是,当你在XAML中完全执行它时,它知道你真的想绑定到CollectionViewSource.View的值并且在后台为你进行绑定(多么有帮助!:/)。
CollectionViewSource.View.Refresh();
以这种方式重新评估CollectionViewSource.Filter!