trapemiyaの日記

hatenablogが新しくなったんで新規一転また2019年1月からちょこちょこ書いてます。C#中心のプログラミングに関するお話です。

CollectionViewSourceを使ったグルーピングあれこれ【その3 GroupNameFromItemオーバーライド時の問題解決編】

前回の、

CollectionViewSourceを使ったグルーピングあれこれ【その2 GroupNameFromItemオーバーライド時の問題】
http://d.hatena.ne.jp/trapemiya/?_ts=1344491065

の続きです。

結局、GroupNameFromItemをオーバーライドして返される値でグルーピングされてしまうので、GroupNameFromItemを使う限りどうしようもないことがわかります。まぁ、その値を例えば、「ランナー名【ランナーID】」のようにすればほぼ間違いなくユニークになりますが、これだと負けのような気がしますし、第一、ランナー名だけ表示したい場合には太刀打ちできません。
よって、GroupNameFromItemをオーバーライドすることはきっぱり止めにします。

では、どうするか? まずはグルーピングされたヘッダに何がバインドしているのかを知らなければ糸口が見つかりません。調べてみると、CollectionViewGroupクラスがバインドしていることがわかります。

CollectionViewGroup クラス
http://msdn.microsoft.com/ja-jp/library/system.windows.data.collectionviewgroup.aspx

このクラスのメンバーを見るとわかるのですが、その数がものすごく少ないんですね。その中にNameプロパティという重要なプロパティがあって、ここにグループヘッダの表示に使用できる値が入るようになっています。GroupNameFromItemをオーバーライドした場合、そこから返される値がNameプロパティにセットされます。なので、以下のようにNameプロパティにバインドさせているわけです。

<DataTemplate x:Key="グループヘッダTemplate">
    <StackPanel Background="#FFCDF2CD">
        <TextBlock Text="{Binding Name}" FontWeight="Bold"/>
        <Rectangle Height="1" Fill="Blue"/>
    </StackPanel>
</DataTemplate>

ここで閃きませんか?閃きますよね。バインドされている値を変えたいならコンバーターを使え!と。
さて、それは良いとして、CollectionViewGroupクラスの何をバインドさせましょうか?Nameプロパティにはグルーピングで指定したランナーIDがセットされています。(GroupNameFromItemのオーバーライドを止めましたからね) というわけでNameプロパティをバインドしても意味がありません。そこで、ランナー名が含まれているプロパティを探すと・・・・・Itemsしか無さそうです。このコレクションに含まれるランナー名は全て同じですし、Itemsには最低1つの要素が存在しますから、Itemsの最初の1個のみをバインドすれば良さそうです。コンバーターの名前は「ランナー名取得Converter」として、以下のようにグループヘッダTemplateを変更します。

<DataTemplate x:Key="グループヘッダTemplate">
    <StackPanel Background="#FFCDF2CD">
        <TextBlock Text="{Binding Path=Items[0], Converter={StaticResource ランナー名取得Converter}}" Margin="3.5,0" FontWeight="Bold"/>
        <Rectangle Height="1" Fill="Blue"/>
    </StackPanel>
</DataTemplate>

あとは、ランナー名取得Converterを作るだけです。

[ValueConversion(typeof(CollectionViewGroup), typeof(string))]
public class ランナー名取得Converter : IValueConverter
{
    #region IValueConverter メンバ

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        dynamic d = value;

        return ((ランナー)d).ランナー名;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    #endregion
}

ここで渡されるvalueの型はMS.Internal.Data.CollectionViewGroupInternalなので、((MS.Internal.Data.CollectionViewGroupInternal)value).Items[0]とすれば実際のランナー型のオブジェクトが得られるのですが、MS.Internal.Data.CollectionViewGroupInternalは、おそらく参照設定できないのではないかと思い(よく調べてませんが・・・)、dynamicで受けています。
以上で実行すると、以下のように表示されます。思った通りですね!

最後にソースを載せておきます。

<Window x:Class="WPFGroupingExamples.グループヘッダコンバーター使用View"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WPFGroupingExamples"
        Title="グループヘッダコンバーター使用View"  Height="393" Width="693">
    
    <Window.DataContext>
        <local:グループヘッダコンバーター使用ViewModel />
    </Window.DataContext>

    <Window.Resources>
        <local:ランナー名取得Converter x:Key="ランナー名取得Converter" />

        <DataTemplate x:Key="グループヘッダTemplate">
            <StackPanel Background="#FFCDF2CD">
                <TextBlock Text="{Binding Path=Items[0], Converter={StaticResource ランナー名取得Converter}}" Margin="3.5,0" FontWeight="Bold"/>
                <Rectangle Height="1" Fill="Blue"/>
            </StackPanel>
        </DataTemplate>

    </Window.Resources>

    <Grid>

        <ListView ItemsSource="{Binding UICollectionViewSource.View, Mode=OneWay}" Margin="0,0,0,37">

            <ListView.GroupStyle>
                <GroupStyle HeaderTemplate="{StaticResource グループヘッダTemplate}"/>
            </ListView.GroupStyle>

            <ListView.View>
                <GridView>
                    <GridView.Columns>
                        <GridViewColumn Header="大会ID" DisplayMemberBinding="{Binding 大会ID}" Width="Auto" />
                        <GridViewColumn Header="大会名" DisplayMemberBinding="{Binding 大会名}" Width="Auto" />
                        <GridViewColumn Header="ランナーID" DisplayMemberBinding="{Binding ランナーID}" Width="Auto" />
                        <GridViewColumn Header="ランナー名" DisplayMemberBinding="{Binding ランナー名}" Width="Auto" />
                        <GridViewColumn Header="タイム" DisplayMemberBinding="{Binding タイム}" Width="Auto" />
                    </GridView.Columns>
                </GridView>
            </ListView.View>
        </ListView>

        <Button Content="ソート・グループ化適用" Command="{Binding ソート・グループ化適応Command, Mode=OneWay}" Height="23" HorizontalAlignment="Left" Margin="100,329,0,0" Name="button1" VerticalAlignment="Top" Width="157" />

    </Grid>

</Window>
using System;
using System.Collections.ObjectModel;
using System.Windows.Data;
using System.ComponentModel;
using System.Windows.Input;
using WPFGroupingExamples.Helpers;

namespace WPFGroupingExamples
{
    class グループヘッダコンバーター使用ViewModel
    {
        #region コンストラクタ

        public グループヘッダコンバーター使用ViewModel()
        {
            ソート・グループ化適応Command = new RelayCommand(ソート・グループ設定);

            ObservableCollection<ランナー> uiobjects = new ObservableCollection<ランナー>();

            uiobjects.Add(new ランナー { 大会ID = 1, 大会名 = "サロマ100km", ランナーID = 1, ランナー名 = "関西太郎", タイム = new TimeSpan(11, 59, 35) });
            uiobjects.Add(new ランナー { 大会ID = 2, 大会名 = "隠岐ウルトラ100km", ランナーID = 1, ランナー名 = "関西太郎", タイム = new TimeSpan(12, 22, 31) });
            uiobjects.Add(new ランナー { 大会ID = 3, 大会名 = "四万十川ウルトラ100km", ランナーID = 1, ランナー名 = "関西太郎", タイム = new TimeSpan(12, 01, 28) });
            uiobjects.Add(new ランナー { 大会ID = 1, 大会名 = "サロマ100km", ランナーID = 2, ランナー名 = "関東洋子", タイム = new TimeSpan(11, 16, 22) });
            uiobjects.Add(new ランナー { 大会ID = 2, 大会名 = "隠岐ウルトラ100km", ランナーID = 2, ランナー名 = "関東洋子", タイム = new TimeSpan(11, 45, 42) });
            uiobjects.Add(new ランナー { 大会ID = 3, 大会名 = "四万十川ウルトラ100km", ランナーID = 2, ランナー名 = "関東洋子", タイム = new TimeSpan(11, 14, 17) });

            uiobjects.Add(new ランナー { 大会ID = 1, 大会名 = "サロマ100km", ランナーID = 3, ランナー名 = "関西太郎", タイム = new TimeSpan(10, 23, 11) });

            UICollectionViewSource = new CollectionViewSource();
            UICollectionViewSource.Source = uiobjects;
        }

        #endregion

        #region プロパティ

        /// <summary>
        /// UIに表示するCollectionViewSourceを取得する。
        /// </summary>
        CollectionViewSource _UICollectionViewSource;
        public CollectionViewSource UICollectionViewSource { get { return _UICollectionViewSource; } set { _UICollectionViewSource = value; } }

        #endregion

        #region コマンド

        /// <summary>
        /// ソートおよびグループ化を実行するコマンド
        /// </summary>
        public ICommand ソート・グループ化適応Command { get; private set; }

        #endregion

        #region コマンドハンドラ

        /// <summary>
        /// ソートおよびグループ化を実行する。
        /// </summary>
        private void ソート・グループ設定(object obj)
        {
            SortDescription sortDescription;
            PropertyGroupDescription groupDescription;

            //ソートの指定
            UICollectionViewSource.SortDescriptions.Clear();

            sortDescription = new SortDescription
            {
                PropertyName = "大会ID",
                Direction = ListSortDirection.Ascending
            };
            UICollectionViewSource.SortDescriptions.Add(sortDescription);

            sortDescription = new SortDescription
            {
                PropertyName = "ランナーID",
                Direction = ListSortDirection.Ascending
            };
            UICollectionViewSource.SortDescriptions.Add(sortDescription);

            //グループの指定
            UICollectionViewSource.GroupDescriptions.Clear();

            //groupDescription = new ランナーGroupDescription
            groupDescription = new PropertyGroupDescription
            {
                PropertyName = "ランナーID"
            };
            UICollectionViewSource.GroupDescriptions.Add(groupDescription);

            UICollectionViewSource.View.Refresh();
        }

        #endregion

        //#region GroupDescription

        //class ランナーGroupDescription : PropertyGroupDescription
        //{
        //    public override object GroupNameFromItem(object item, int level, System.Globalization.CultureInfo culture)
        //    {
        //        var uiobject = (ランナー)item;

        //        return uiobject.ランナー名;
        //    }
        //}

        //#endregion
    }

    #region コンバーター

    [ValueConversion(typeof(CollectionViewGroup), typeof(string))]
    public class ランナー名取得Converter : IValueConverter
    {
        #region IValueConverter メンバ

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            dynamic d = value;

            return ((ランナー)d).ランナー名;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        #endregion
    }

    #endregion

    #region ランナー クラス

    class ランナー
    {
        public int 大会ID { get; set; }
        public string 大会名 { get; set; }
        public int ランナーID { get; set; }
        public string ランナー名 { get; set; }
        public TimeSpan タイム { get; set; }
    }

    #endregion
}