1

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

有一个带有 DataGrid 的下部面板,以及一个带有一些 ListViews 显示分组行的上部面板。当我在上面板的“流派”列表中单击“摇滚”时,其他列表会更新,DataGrid 也会相应地进行过滤。如果我继续单击上方面板中的其他列表,DataGrid 过滤会变得越来越严格,并会继续进行相应更新(仅显示与上面的过滤器匹配的行)。

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

在此处输入图像描述

我开始阅读有关ListCollectionView课程的内容,因为它的文档说:

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

在我看来,分组和过滤就是我想要在这里完成的全部内容,但我发现缺乏示例,甚至不知道从哪里开始,无论是 ViewModel 端还是 XAML 端。

4

1 回答 1

3

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

假设您有一个看起来像这样的轨道的视图模型:

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();
        }
    }
}

这是测试应用程序的屏幕截图:

截屏

我不知道这是否满足您正在寻找的功能要求,但希望它至少可以成为您尝试做的各种事情的一个很好的参考。

于 2015-06-11T06:34:43.727 回答