如何在WPF中执行ListViews和DataGrids的同步分组/过滤?

时间:2015-06-10 14:31:41

标签: wpf listview collectionviewsource listcollectionview

我试图在我的应用程序中创建软件MusicBee在其音乐选择界面中使用的相同效果(下面的屏幕截图)。

下面的面板带有DataGrid,上面板有一些ListViews显示分组行。 当我在上面板的“类型”列表中单击,例如“摇滚”时,其他列表将更新,并相应地过滤DataGrid。如果我继续点击上面板中的其他列表,DataGrid过滤会变得越来越严格,并继续相应地更新(仅显示与上述过滤器匹配的行)。

此外,还有额外的行:All (N items)[Empty],我想这些行必须以某种方式添加到视图源中。

enter image description here

我开始阅读ListCollectionView类,因为它的文档说:

  

“当您绑定到数据集合时,您可能希望对数据进行排序,过滤或分组。为此,您可以使用集合视图。”

在我看来,分组和过滤都是关于我想在这里完成的事情,但我发现缺少示例,甚至不知道从哪里开始,无论是ViewModel端还是XAML端。 / p>

1 个答案:

答案 0 :(得分:2)

这是一个非常广泛的问题,因此我将向您展示一种可以实现您正在寻找的内容的方法。当然有多种方法可以达到相同的效果。这种方式恰好跟随你已经尝试使用的东西。我也不知道它是否涵盖了您正在寻找的所有功能。

假设你有一个类似于这样的轨道的视图模型:

internal class Track
{
    public string Genre { get; private set; }
    public string Artist { get; private set; }
    public string Album { get; private set; }
    public string Title { get; private set; }
    public string FileName { get; private set; }

    public Track(string genre, string artist, string album, string title, string fileName)
    {
        Genre = genre;
        Artist = artist;
        Album = album;
        Title = title;
        FileName = fileName;
    }
}

您需要为整个视图创建一个视图模型,其中包含这些轨道的可观察集合,该集合的集合视图以及过滤器的其他集合(屏幕截图的顶部)。我在本地扔了一些东西,结果看起来像这样(需要一些清理):

internal class MainWindowVM : INotifyPropertyChanged
{
    // Persistent filter values
    private static readonly FilterValue EmptyFilter;
    private static readonly FilterValue AllFilter;
    private static readonly FilterValue[] CommonFilters;

    private ObservableCollection<Track> mTracks;
    private ListCollectionView mTracksView;

    private FilterValue mSelectedGenre;
    private FilterValue mSelectedArtist;
    private FilterValue mSelectedAlbum;

    private bool mIsRefreshingView;

    public ICollectionView Tracks { get { return mTracksView; } }

    public IEnumerable<FilterValue> Genres
    {
        get { return CommonFilters.Concat(mTracksView.Groups.Select(g => new FilterValue((CollectionViewGroup)g))); }
    }

    public IEnumerable<FilterValue> Artists
    {
        get
        {
            if (mSelectedGenre != null)
            {
                if (mSelectedGenre.Group != null)
                {
                    return CommonFilters.Concat(mSelectedGenre.Group.Items.Select(g => new FilterValue((CollectionViewGroup)g)));
                }
                else if (mSelectedGenre == AllFilter)
                {
                    return CommonFilters.Concat(mTracksView.Groups.SelectMany(genre => ((CollectionViewGroup)genre).Items.Select(artist => new FilterValue((CollectionViewGroup)artist))));
                }
            }
            return new FilterValue[] { EmptyFilter };
        }
    }

    public IEnumerable<FilterValue> Albums
    {
        get
        {
            if (mSelectedArtist != null)
            {
                if (mSelectedArtist.Group != null)
                {
                    return CommonFilters.Concat(mSelectedArtist.Group.Items.Select(g => new FilterValue((CollectionViewGroup)g)));
                }
                else if (mSelectedArtist == AllFilter)
                {
                    // TODO: This is getting out of hand at this point. More groups will make it even worse. Should handle this in a better way.
                    return CommonFilters.Concat(mTracksView.Groups.SelectMany(genre => ((CollectionViewGroup)genre).Items.SelectMany(artist => ((CollectionViewGroup)artist).Items.Select(album => new FilterValue((CollectionViewGroup)album)))));
                }
            }
            return new FilterValue[] { EmptyFilter };
        }
    }

    // The following "Selected" properties assume that only one group can be selected
    // from each category. These should probably be expanded to allow for selecting
    // multiple groups from the same category.

    public FilterValue SelectedGenre
    {
        get { return mSelectedGenre; }
        set
        {
            if (!mIsRefreshingView && mSelectedGenre != value)
            {
                mSelectedGenre = value;
                RefreshView();
                NotifyPropertyChanged("SelectedGenre", "Artists");
            }
        }
    }

    public FilterValue SelectedArtist
    {
        get { return mSelectedArtist; }
        set
        {
            if (!mIsRefreshingView && mSelectedArtist != value)
            {
                mSelectedArtist = value;
                RefreshView();
                NotifyPropertyChanged("SelectedArtist", "Albums");
            }
        }
    }

    public FilterValue SelectedAlbum
    {
        get { return mSelectedAlbum; }
        set
        {
            if (!mIsRefreshingView && mSelectedAlbum != value)
            {
                mSelectedAlbum = value;
                RefreshView();
                NotifyPropertyChanged("SelectedAlbum");
            }
        }
    }

    static MainWindowVM()
    {
        EmptyFilter = new FilterValue("[Empty]");
        AllFilter = new FilterValue("All");
        CommonFilters = new FilterValue[]
        {
            EmptyFilter,
            AllFilter
        };
    }

    public MainWindowVM()
    {
        // Prepopulating test data
        mTracks = new ObservableCollection<Track>()
        {
            new Track("Genre 1", "Artist 1", "Album 1", "Track 1", "01 - Track 1.mp3"),
            new Track("Genre 2", "Artist 2", "Album 1", "Track 2", "02 - Track 2.mp3"),
            new Track("Genre 1", "Artist 1", "Album 1", "Track 3", "03 - Track 3.mp3"),
            new Track("Genre 1", "Artist 3", "Album 2", "Track 4", "04 - Track 4.mp3"),
            new Track("Genre 2", "Artist 2", "Album 2", "Track 5", "05 - Track 5.mp3"),
            new Track("Genre 3", "Artist 4", "Album 1", "Track 1", "01 - Track 1.mp3"),
            new Track("Genre 3", "Artist 4", "Album 4", "Track 2", "02 - Track 2.mp3"),
            new Track("Genre 1", "Artist 3", "Album 1", "Track 3", "03 - Track 3.mp3"),
            new Track("Genre 2", "Artist 2", "Album 3", "Track 4", "04 - Track 4.mp3"),
            new Track("Genre 2", "Artist 5", "Album 1", "Track 5", "05 - Track 5.mp3"),
            new Track("Genre 1", "Artist 1", "Album 2", "Track 6", "06 - Track 6.mp3"),
            new Track("Genre 3", "Artist 4", "Album 1", "Track 7", "07 - Track 7.mp3")
        };

        mTracksView = (ListCollectionView)CollectionViewSource.GetDefaultView(mTracks);

        // Note that groups are hierarchical. Based on this setup, having tracks with
        // the same artist but different genres would place them in different groups.
        // Grouping might not be the way to go here, but it gives us the benefit of
        // auto-generating groups based on the values of properties in the collection.
        mTracksView.GroupDescriptions.Add(new PropertyGroupDescription("Genre"));
        mTracksView.GroupDescriptions.Add(new PropertyGroupDescription("Artist"));
        mTracksView.GroupDescriptions.Add(new PropertyGroupDescription("Album"));

        mTracksView.Filter = FilterTrack;

        mSelectedGenre = EmptyFilter;
        mSelectedArtist = EmptyFilter;
        mSelectedAlbum = EmptyFilter;
    }

    private void RefreshView()
    {
        // Refreshing the view will cause all of the groups to be deleted and recreated, thereby killing
        // our selected group. We will track when a refresh is happening and ignore those group changes.
        if (!mIsRefreshingView)
        {
            mIsRefreshingView = true;
            mTracksView.Refresh();
            mIsRefreshingView = false;
        }
    }

    private bool FilterTrack(object obj)
    {
        Track track = (Track)obj;
        Func<FilterValue, string, bool> filterGroup = (filter, trackName) => filter == null || filter.Group == null || trackName == (string)filter.Group.Name;
        return
            filterGroup(mSelectedGenre, track.Genre) &&
            filterGroup(mSelectedArtist, track.Artist) &&
            filterGroup(mSelectedAlbum, track.Album);
    }

    #region INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
    protected void NotifyPropertyChanged(params string[] propertyNames)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            foreach (String propertyName in propertyNames)
            {
                handler.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
    #endregion
}

internal class FilterValue
{
    private string mName;

    public CollectionViewGroup Group { get; set; }
    public string Name { get { return Group != null ? Group.Name.ToString() : mName; } }

    public FilterValue(string name)
    {
        mName = name;
    }

    public FilterValue(CollectionViewGroup group)
    {
        Group = group;
    }

    public override string ToString()
    {
        return Name;
    }
}

我用于此的视图有一个每个过滤器的列表框和一个显示轨道的底部的数据网格。

<Window x:Class="WPFApplication1.MainWindow"
        x:ClassModifier="internal"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WPFApplication1"
        Title="MainWindow" Height="600" Width="800">
    <Window.DataContext>
        <local:MainWindowVM />
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="5" />
            <RowDefinition Height="2*" />
        </Grid.RowDefinitions>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="auto" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <Border
                BorderThickness="1 1 0 0"
                SnapsToDevicePixels="True"
                BorderBrush="{x:Static SystemColors.ControlDarkDarkBrush}">
                <TextBlock
                    Margin="4 1"
                    Text="Genre" />
            </Border>
            <Border
                Grid.Column="1"
                Margin="-1 0 0 0"
                BorderThickness="1 1 0 0"
                SnapsToDevicePixels="True"
                BorderBrush="{x:Static SystemColors.ControlDarkDarkBrush}">
                <TextBlock
                    Margin="4 1"
                    Text="Artist" />
            </Border>
            <Border
                Grid.Column="2"
                Margin="-1 0 0 0"
                BorderThickness="1 1 1 0"
                SnapsToDevicePixels="True"
                BorderBrush="{x:Static SystemColors.ControlDarkDarkBrush}">
                <TextBlock
                    Margin="4 1"
                    Text="Album" />
            </Border>
            <ListBox
                Grid.Row="1"
                ItemsSource="{Binding Genres}"
                SelectedItem="{Binding SelectedGenre, UpdateSourceTrigger=Explicit}"
                SelectionChanged="ListBox_SelectionChanged" />
            <ListBox
                Grid.Row="1"
                Grid.Column="1"
                ItemsSource="{Binding Artists}"
                SelectedItem="{Binding SelectedArtist, UpdateSourceTrigger=Explicit}"
                SelectionChanged="ListBox_SelectionChanged" />
            <ListBox
                Grid.Row="1"
                Grid.Column="2"
                ItemsSource="{Binding Albums}"
                SelectedItem="{Binding SelectedAlbum, UpdateSourceTrigger=Explicit}"
                SelectionChanged="ListBox_SelectionChanged" />
        </Grid>
        <GridSplitter
            Grid.Row="1"
            HorizontalAlignment="Stretch"
            VerticalAlignment="Stretch" />
        <DataGrid
            Grid.Row="2"
            ItemsSource="{Binding Tracks}" />
    </Grid>
</Window>

这是视图的代码隐藏。我只需要在视图中更改选择时更新视图模型中的过滤器选择。否则,由于某种原因,它最终会将其设置为null。我没有花时间调查造成这个问题的原因。我只是在选择改变时明确更新源代码来解决它。

internal partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        var expression = BindingOperations.GetBindingExpression((DependencyObject)sender, Selector.SelectedItemProperty);
        if (expression != null)
        {
            expression.UpdateSource();
        }
    }
}

以下是测试应用的截图:

Screenshot

我不知道这是否符合您所寻找的功能要求,但希望它至少可以成为您尝试做的各种事情的良好参考。