52

我正在使用 MVVM 模式开发 WPF 桌面应用程序。

我正在尝试ListView根据在 a 中键入的文本从 a 中过滤出一些项目TextBox。我希望在ListView更改文本时过滤项目。

我想知道当过滤器文本改变时如何触发过滤器。

ListView绑定到 a CollectionViewSource,它绑定到我的ObservableCollectionViewModel 上的 。过滤器文本的TextBox绑定到 ViewModel 上的字符串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 上的过滤器方法。

当 FilterText 的值发生变化时,过滤就完成了——FilterText 属性的设置器调用了一个 FilterList 方法,该方法ObservableCollection在我的 ViewModel 中迭代并在每个项目 ViewModel 上设置一个booleanFilteredOut 属性。

我知道 FilteredOut 属性会在过滤器文本更改时更新,但列表不会刷新。CollectionViewSource只有当我重新加载 UserControl 时,过滤器事件才会被触发。

OnPropertyChanged("AllProjects")在更新过滤器信息后尝试调用,但它并没有解决我的问题。(“AllProjects”是绑定ObservableCollection到我的 ViewModel 上的属性CollectionViewSource。)

CollectionViewSource当 FilterText 的值发生变化时,如何让自己重新过滤TextBox

非常感谢

4

6 回答 6

78

不要CollectionViewSource在您的视图中创建一个。相反,在您的视图模型中创建一个类型的属性ICollectionView并绑定ListView.ItemsSource到它。

完成此操作后,您可以将逻辑放入属性的设置器中,该设置器在用户更改时FilterText调用。Refresh()ICollectionView

您会发现这也简化了排序问题:您可以将排序逻辑构建到视图模型中,然后公开视图可以使用的命令。

编辑

这是一个使用 MVVM 对集合视图进行动态排序和过滤的非常简单的演示。这个演示没有实现FilterText,但是一旦你理解了它是如何工作的,你应该不会有任何困难来实现一个FilterText属性和一个使用该属性而不是它现在使用的硬编码过滤器的谓词。

(还要注意,这里的视图模型类不实现属性更改通知。这只是为了保持代码简单:由于此演示中没有任何内容实际更改属性值,因此不需要属性更改通知。)

首先为您的项目分类:

public class ItemViewModel
{
    public string Name { get; set; }
    public int Age { get; set; }
}

现在,应用程序的视图模型。这里发生了三件事:首先,它创建并填充自己的ICollectionView; 其次,它公开了一个ApplicationCommand视图将用来执行排序和过滤命令的(见下文),最后,它实现了一个Execute排序或过滤视图的方法:

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的 on 按钮路由CommandParameter到视图模型的Execute方法。我以这种方式实现它是因为我不想RelayCommand在应用程序视图模型中创建一堆属性,并且我想将所有排序/过滤保留在一个方法中,以便轻松查看它是如何完成的。

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>
于 2011-06-24T15:42:08.567 回答
26

如今,您通常不需要显式触发刷新。如果为真,则根据其集合中的字段CollectionViewSource实现自动更新。ICollectionViewLiveShapingIsLiveFilteringRequestedLiveFilteringProperties

XAML 中的一个示例:

  <CollectionViewSource
         Source="{Binding Items}"
         Filter="FilterPredicateFunction"
         IsLiveFilteringRequested="True">
    <CollectionViewSource.LiveFilteringProperties>
      <system:String>FilteredProperty1</system:String>
      <system:String>FilteredProperty2</system:String>
    </CollectionViewSource.LiveFilteringProperties>
  </CollectionViewSource>
于 2015-06-09T17:03:01.643 回答
11
CollectionViewSource.View.Refresh();

CollectionViewSource.Filter 以这种方式重新评估!

于 2015-06-10T12:22:46.280 回答
6

也许你在你的问题中简化了你的视图,但正如所写,你真的不需要 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 中,当然也不希望出现在我的模型中。

  • 在我的示例中,我使用“Filter In”而不是“Filter Out”。在我看来(在大多数情况下)我正在应用的过滤器是我确实想看到的东西,这似乎更合乎逻辑。如果您真的想过滤掉内容,只需否定 Contains 子句(即 item => ! Item.Text.Contains(...))。

  • 您可能有一种更集中的方式在您的 ViewModel 中执行您的集合。要记住的重要一点是,当您更改 FilterText 时,您还需要通知您的 AllFilteredItems 集合。我在这里内联做了,但您也可以处理 PropertyChanged 事件并在 e.PropertyName 为 FilterText 时调用 PropertyChanged。

如果您需要任何澄清,请告诉我。

于 2011-06-24T13:34:03.687 回答
2

如果我理解你的要求:

在您的财产的 set 部分中,FilterText只需调用Refresh()您的CollectionView.

于 2011-06-24T12:26:27.417 回答
2

我刚刚发现了一个更优雅的解决这个问题的方法。而不是在您的 ViewModel中创建一个ICollectionView(正如接受的答案所暗示的那样)并将您的绑定设置为

ItemsSource={Binding Path=YourCollectionViewSourceProperty}

CollectionViewSource更好的方法是在您的 ViewModel中创建一个属性。然后绑定你ItemsSource如下

ItemsSource={Binding Path=YourCollectionViewSourceProperty.View}    

注意添加.View这样一ItemsSource只要对CollectionViewSourceRefresh()ICollectionView

注意:我无法确定为什么会这样。如果直接绑定到CollectionViewSource属性,则绑定失败。但是,如果您在 XAML 文件的元素中定义 aCollectionViewSourceResources直接绑定到资源键,则绑定工作正常。我唯一能猜到的是,当您完全在 XAML 中执行此操作时,它知道您确实想要绑定到 CollectionViewSource.View 值并在幕后为您绑定它(多么有帮助!:/)。

于 2014-10-10T16:56:12.480 回答