CollectionViewSourceのグループ化機能

 今回はCollectionViewSourceのグループ化機能を使ってグループ毎にアイテムを表示する方法を説明します。
 今回もXAML上でグループ化の定義を行う場合とViewModelで動的にグループ化の定義を行う方法を紹介します。
 Modelのコードは以下の様になっています。
using Livet;
using System.Collections.ObjectModel;

namespace CollectionViewSourceDemo3.Models
{
    public class Library : NotificationObject
    {
        public Library()
        {
            BookList = new ObservableCollection<Book>();
            BookList.Add(new Book("オーバーロード1", "オーバーロード", "丸山くがね"));
            BookList.Add(new Book("オーバーロード2", "オーバーロード", "丸山くがね"));
            BookList.Add(new Book("オーバーロード3", "オーバーロード", "丸山くがね"));
            BookList.Add(new Book("オーバーロード4", "オーバーロード", "丸山くがね"));
            BookList.Add(new Book("鋼の錬金術師1", "鋼の錬金術師", "荒川弘"));
            BookList.Add(new Book("鋼の錬金術師2", "鋼の錬金術師", "荒川弘"));
            BookList.Add(new Book("鋼の錬金術師3", "鋼の錬金術師", "荒川弘"));
            BookList.Add(new Book("銀の匙1", "銀の匙", "荒川弘"));
            BookList.Add(new Book("銀の匙2", "銀の匙", "荒川弘"));
            BookList.Add(new Book("アルスラーン戦記1", "アルスラーン戦記", "荒川弘"));
        }

        public ObservableCollection<Book> BookList { get; private set; }
    }

    public class Book : NotificationObject
    {
        public Book(string title, string series, string author)
        {
            Title = title;
            Series = series;
            Author = author;
        }

        public string Title { get; private set; }
        public string Author { get; private set; }
        public string Series { get; private set; }
    }
}
 Modelでは本のデータを用意しているだけです。
 本のタイトル等を変更することはないので変更通知プロパティにしていません。

 次にViewModelのコードです。
using CollectionViewSourceDemo3.Models;
using Livet;
using System.Windows.Data;

namespace CollectionViewSourceDemo3.ViewModels
{
    public class MainWindowViewModel : ViewModel
    {
        private Library BookLibrary;

        public MainWindowViewModel()
        {
            BookLibrary = new Library();
            CanGrouping = true;
            CanUngroup = false;

            BookList = ViewModelHelper.CreateReadOnlyDispatcherCollection(
                BookLibrary.BookList,
                m => new BookViewModel(m),
                DispatcherHelper.UIDispatcher);
            CompositeDisposable.Add(BookList);

            BookListViewSource = new CollectionViewSource()
            {
                Source = BookList
            };
        }

        public ReadOnlyDispatcherCollection<BookViewModel> BookList { get; private set; }

        public CollectionViewSource BookListViewSource { get; private set; }


        #region CanGrouping変更通知プロパティ
        private bool _CanGrouping;

        public bool CanGrouping
        {
            get
            { return _CanGrouping; }
            private set
            { 
                if (_CanGrouping == value)
                    return;
                _CanGrouping = value;
                RaisePropertyChanged();
            }
        }
        #endregion


        #region CanUngroup変更通知プロパティ
        private bool _CanUngroup;

        public bool CanUngroup
        {
            get
            { return _CanUngroup; }
            private set
            { 
                if (_CanUngroup == value)
                    return;
                _CanUngroup = value;
                RaisePropertyChanged();
            }
        }
        #endregion


        /// <summary>
        /// グループ分けする
        /// </summary>
        public void Grouping()
        {
            CanGrouping = false;
            BookListViewSource.GroupDescriptions.Add(new PropertyGroupDescription("Author"));
            BookListViewSource.GroupDescriptions.Add(new PropertyGroupDescription("Series"));
            CanUngroup = true;
        }

        /// <summary>
        /// グループ化解除
        /// </summary>
        public void Ungroup()
        {
            CanUngroup = false;
            BookListViewSource.GroupDescriptions.Clear();
            CanGrouping = true;
        }
    }

    public class BookViewModel : ViewModel
    {
        private Book Book;

        public BookViewModel(Book book)
        {
            Book = book;
        }

        public string Title { get { return Book.Title; } }
        public string Author { get { return Book.Author; } }
        public string Series { get { return Book.Series; } }
    }
}
 BookViewModelはBook(Modelクラス)のプロパティを公開しているだけです。
 MainWindowViewModelでは本のリスト(CollectionViewSourceのソース)とCollectionViewSourceを作っています。
 GroupingメソッドはCollectionViewSourceのグループ化定義を行うメソッドです。
 CollectionViewSourceのグループ定義はGroupDescriptionsにグループ化条件を追加するだけです。(ソートの時と同じような感じです)
 プロパティ名によるグループ化を行う場合はPropertyGroupDescriptionを使用します。
 サンプルコードの様に複数の条件を指定することもできます。
 この場合、まず「Auther」でグループ化され、子グループ内でさらに「Series」でグループ化が行われます。
 グループ化を解除したい場合はGroupDescriptionsをクリアするだけでOKです。

 最後にViewのコードです。
<Window x:Class="CollectionViewSourceDemo3.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:CollectionViewSourceDemo3.Views"
        xmlns:vm="clr-namespace:CollectionViewSourceDemo3.ViewModels"
        xmlns:data="clr-namespace:System.Windows.Data;assembly=PresentationFramework"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    
    <i:Interaction.Triggers>
        <!--Windowが閉じたタイミングでViewModelのDisposeメソッドが呼ばれます-->
        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    
    <Window.Resources>
        <DataTemplate DataType="{x:Type vm:BookViewModel}">
            <TextBlock>
                【<Run Text="{Binding Series, Mode=OneWay}"/>】
                 <Run Text="{Binding Title, Mode=OneWay}"/> 
                (<Run Text="{Binding Author, Mode=OneWay}"/>)
            </TextBlock>
        </DataTemplate>
        
        <DataTemplate x:Key="GroupHeadderTemplate">
            <Border Background="WhiteSmoke" BorderThickness="0,0,0,2" BorderBrush="DarkBlue" >
                <TextBlock Text="{Binding Name}" Foreground="DarkBlue" FontSize="16"/>
            </Border>
        </DataTemplate>
                
        <CollectionViewSource x:Key="group" Source="{Binding BookList}">
            <CollectionViewSource.GroupDescriptions>
                <data:PropertyGroupDescription PropertyName="Author"/>
                <data:PropertyGroupDescription PropertyName="Series"/>
            </CollectionViewSource.GroupDescriptions>
        </CollectionViewSource>
    </Window.Resources>
    
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <GroupBox Grid.Column="0" Header="XAMLでグループ化定義">
            <ListBox ItemsSource="{Binding Source={StaticResource group}}">
                <ListBox.GroupStyle>
                    <!--規定のグループスタイル-->
                    <x:Static Member="GroupStyle.Default"/>
                </ListBox.GroupStyle>
            </ListBox>
        </GroupBox>
        <GroupBox Grid.Column="1" Header="ViewModelでグループ化定義">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="*"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                <ListBox ItemsSource="{Binding BookListViewSource.View}">
                    <ListBox.GroupStyle>
                        <GroupStyle HeaderTemplate="{StaticResource GroupHeadderTemplate}"/>
                    </ListBox.GroupStyle>
                </ListBox>
                <StackPanel Grid.Row="1" HorizontalAlignment="Right" Orientation="Horizontal">
                    <Button Content="グループ化" IsEnabled="{Binding CanGrouping}">
                        <i:Interaction.Triggers>
                            <i:EventTrigger EventName="Click">
                                <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Grouping"/>
                            </i:EventTrigger>
                        </i:Interaction.Triggers>
                    </Button>
                    <Button Content="グループ化解除" IsEnabled="{Binding CanUngroup}">
                        <i:Interaction.Triggers>
                            <i:EventTrigger EventName="Click">
                                <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Ungroup"/>
                            </i:EventTrigger>
                        </i:Interaction.Triggers>
                    </Button>
                </StackPanel>
            </Grid>
        </GroupBox>
    </Grid>
</Window>
 このサンプルコードを実行し、グループ化させると以下の様になります。
CollectionViewSource グループ化
 XAMLでグループ化の定義を行う場合もGroupDescriptionsにPropertyGroupDescriptionを指定するだけです。
 PropertyGroupDescriptionを使用したい場合はxmlnsにclr-namespace:System.Windows.Data;assembly=PresentationFrameworkを追加してください。
 グループ化を行う場合はGroupStyleを指定する必要があります。
 これを行わないとグループ化しても見た目に変化がありません。
 規定のGroupStyleを使用する場合は以下の様にします。
<ListBox.GroupStyle>
    <x:Static Member="GroupStyle.Default"/>
</ListBox.GroupStyle>
 もちろん、グループスタイルを変更して見た目を変更することもできます。
 このサンプルコードではグループヘッダーを以下の様に定義して使用しています。
<DataTemplate x:Key="GroupHeadderTemplate">
    <Border Background="WhiteSmoke" BorderThickness="0,0,0,2" BorderBrush="DarkBlue" >
        <TextBlock Text="{Binding Name}" Foreground="DarkBlue" FontSize="16"/>
    </Border>
</DataTemplate>
 このテンプレートをGroupStyleのHeaderTemplateに指定すればOKです。

 並び替えやフィルターと違って、条件設定だけでなくグループ化した際の見た目(GroupStyle)を定義・指定しないとグループ化表示ができない点に注意してください。

----- 11/7修正 -----
アルスラーン戦記のシリーズ名が間違っていたのでサンプルコードと画像を修正しました。
スポンサーサイト

テーマ : プログラミング
ジャンル : コンピュータ

CollectionViewSourceのフィルター機能

 CollectionViewSourceには指定した条件を満たす項目のみを表示するフィルター機能も持っています。
 フィルター機能も並べ替えと同じく元コレクションは変更させず表示用のViewのみ変更させています。
 以下のサンプルではCollectionViewSourceのフィルター機能を使って本のタイトル検索を行っています。
 Modelは前回の並べ替えの時と同じなので省略します(本の情報を表すModelがあるだけです)。
using CollectionViewSourceDemo2.Models;
using Livet;
using System.ComponentModel;
using System.Windows.Data;
using Livet.EventListeners;

namespace CollectionViewSourceDemo2.ViewModels
{
    public class MainWindowViewModel : ViewModel
    {
        private Model Model;

        public MainWindowViewModel()
        {
            Model = new Model();
            Keyword = "";

            // ModelコレクションをViewModelコレクションに変換
            Books = ViewModelHelper.CreateReadOnlyDispatcherCollection(
                Model.Books,
                m => new BookViewModel(m),
                DispatcherHelper.UIDispatcher);
            CompositeDisposable.Add(Books);

            // CollectionViewSource作成
            BooksViewSource = new CollectionViewSource() { Source = Books };

            // タイトル検索用フィルター
            CompositeDisposable.Add(new EventListener<FilterEventHandler>(
                h => BooksViewSource.Filter += h,
                h => BooksViewSource.Filter -= h,
                (sender, e) =>
                {
                    // キーワードが空ならすべて表示
                    var book = (BookViewModel)e.Item;
                    e.Accepted = string.IsNullOrWhiteSpace(Keyword)
                        ? true
                        : book.Title.Contains(Keyword);
                }));
        }

        public ReadOnlyDispatcherCollection<BookViewModel> Books { get; private set; }

        public CollectionViewSource BooksViewSource { get; private set; }

        #region Keyword変更通知プロパティ
        private string _Keyword;
        /// <summary>
        /// タイトル検索キーワード
        /// </summary>
        public string Keyword
        {
            get
            { return _Keyword; }
            set
            {
                if (_Keyword == value)
                    return;
                _Keyword = value;
                RaisePropertyChanged();
            }
        }
        #endregion

        /// <summary>
        /// タイトル検索を行う。
        /// </summary>
        public void TitleSearch()
        {
            BooksViewSource.View.Refresh();
        }

        /// <summary>
        /// タイトル検索結果をクリアする
        /// </summary>
        public void ClearSearchResult()
        {
            Keyword = "";
            TitleSearch();
        }
    }

    public class BookViewModel : ViewModel
    {
        private Book Book;

        public BookViewModel(Book book)
        {
            Book = book;
        }

        public string Title
        {
            get { return Book.Title; }
        }

        public string Author
        {
            get { return Book.Author; }
        }

        public int Price
        {
            get { return Book.Price; }
        }
    }
}
 BookViewModelはModelのプロパティを公開しているだけです。(前回と同じ)
 CollectionViewSourceのフィルター機能はFilterイベントを使って行われます。
 サンプルコードではLivetの機能を使用しているので見慣れない形をしているかもしれませんが、CollectionViewSourceのFilterイベントにイベントハンドラを登録しているだけです。
 あとはそのイベントハンドラで項目を表示するかどうかを判断するようなコードを記載します。
 Filterイベントハンドラでは引数としてFilterEventArgsが渡されます。
 このイベント変数はAcceptedプロパティとItemプロパティを持っています。
 Acceptedプロパティはフィルターの結果を表すプロパティでこの値がtrueの項目が表示されます。
 Itemプロパティには評価対象のインスタンスが入っています。(object型なのでキャストする必要があります)
 Itemをキャストして表示するかどうかを評価し、その結果をAcceptedプロパティに設定します。
 ここでは、Titleが指定したキーワードを含んでいるかどうかで表示するかどうかを判断しています。
 キーワードが指定されていない場合は全表示としています。

 これでフィルター機能は使えるようになっていますが、これだけだとCollectionViewが更新されないので適切なタイミングでCollectionViewを更新する必要があります。
 このサンプルでは「検索」ボタンを押した際にCollectionViewを更新するようにしています。
 CollectionViewの更新を行うにはCollectionViewインスタンスのRefreshメソッドを呼ぶだけでOKです。
 RefreshメソッドはUIスレッドから呼び出さないと例外が発生するので注意してください。

<Window x:Class="CollectionViewSourceDemo2.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:CollectionViewSourceDemo2.Views"
        xmlns:vm="clr-namespace:CollectionViewSourceDemo2.ViewModels"
        Title="MainWindow" Height="250" Width="625">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>

    <i:Interaction.Triggers>
        <!--Windowが閉じたタイミングでViewModelのDisposeメソッドが呼ばれます-->
        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>
    </i:Interaction.Triggers>

    <Window.Resources>
        <DataTemplate DataType="{x:Type vm:BookViewModel}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Title}" Margin="0,0,10,0"/>
                <TextBlock Text="{Binding Author, StringFormat=作者:{0}}" Margin="0,0,10,0"/>
                <TextBlock Text="{Binding Price, StringFormat=価格:{0}円}"/>
            </StackPanel>
        </DataTemplate>
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <TextBlock Grid.Column="0" Text="検索ワード:"/>
            <TextBox Grid.Column="1" Text="{Binding Keyword}"/>
            <Button Grid.Column="2" Content="検索">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="TitleSearch"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </Button>
            <Button Grid.Column="3" Content="クリア">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="ClearSearchResult"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </Button>
        </Grid>

        <GroupBox Grid.Row="1" Grid.Column="0" Header="元コレクション">
            <ListBox ItemsSource="{Binding Books}"/>
        </GroupBox>
        <GroupBox Grid.Row="1" Grid.Column="1" Header="ViewModelで作ったCollectionViewSource">
            <ListBox ItemsSource="{Binding BooksViewSource.View}"/>
        </GroupBox>
    </Grid>
</Window>
 Viewでは元のコレクションとCollectionViewの2つを表示しています。
 検索ワードを入力して「検索」ボタンを押すとCollectionViewのリストのみが更新されます。(元コレクションに変化がないことが確認できます)
 以下は「きんいろ」でタイトル検索した結果です。
CollectionViewSource-Filter.png
 元のコレクションを再度表示したい場合は全ての項目が表示されるような条件を入力してCollectionViewを再評価します。
 フィルター機能自体を削除したい場合は、Filterイベントからイベントハンドラを解除すればフィルター機能なしのCollectionViewSourceに戻ります。
 

テーマ : プログラミング
ジャンル : コンピュータ

カレンダー
09 | 2015/10 | 11
- - - - 1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
全記事表示リンク

全ての記事を表示する

カテゴリ
タグリスト

月別アーカイブ
08  07  06  05  04  03  02  01  12  11  10  09  08  07  06  04  03  02  01  12  11  10  09  08  07  06  05  04  03  02  01  12  11  10  09 
最新記事
リンク
最新コメント
検索フォーム
Amazon