1

私のアプリケーションで、音楽選択インターフェイスでソフトウェアMusicBeeが使用するのと同じ効果を作成しようとしています (下のスクリーンショット)。

DataGrid を含む下部パネルと、グループ化された行を表示するいくつかの ListView を含む上部パネルがあります。上部パネルの [ジャンル] リストで [ロック] をクリックすると、他のリストが更新され、それに応じて DataGrid がフィルター処理されます。上部パネルの他のリストをクリックし続けると、DataGrid フィルタリングはますます制限的になり、それに応じて更新されます (上記のフィルターに一致する行のみが表示されます)。

また、余分な行があります:All (N items)[Empty]、どうにかしてビュー ソースに追加する必要があると思います。

ここに画像の説明を入力

ListCollectionViewドキュメントに次のように記載されているため、クラスについて読み始めました。

「データ コレクションにバインドする場合、データの並べ替え、フィルター処理、またはグループ化が必要になる場合があります。これを行うには、コレクション ビューを使用します。」

グループ化とフィルタリングは、ここで達成したいことのすべてのように思えますが、例が不足しており、ViewModel 側または XAML 側のどちらから始めればよいかさえわかりません。

4

1 に答える 1

3

これは非常に幅広い質問なので、探しているようなものを実装する方法を 1 つだけ示します。もちろん、同じ結果を得る方法は複数あります。この方法は、すでに使用しようとしていたものにたまたま従うだけです。また、あなたが探しているすべての機能をカバーしているかどうかもわかりません.

次のようなトラックのビューモデルがあるとします。

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 に答える