これは非常に幅広い質問なので、探しているようなものを実装する方法を 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();
}
}
}
テストアプリのスクリーンショットは次のとおりです。

これがあなたが探している機能の要件を満たしているかどうかはわかりませんが、少なくとも、あなたがしようとしている種類のことを行う方法の良い参考になることを願っています.