グループ化したコレクションのヘッダーをスクロール時に常に表示する

## 基本のおさらい

まずは基本的な部分の確認です。
１．CollectionViewSourceを用意
２．ItemsControlでItemsSourceに１で用意したCollectionViewSourceをバインド
３．スタイルでグループヘッダーの見た目を定義

具体的には
１．リソースにCollectionViewSourceを定義
PropertyGroupDescriptionでグループ化したいプロパティ名称を指定します。

```xml
<UserControl.Resources>
    <CollectionViewSource x:Key="StaffCollectionKey"
                          Source="{Binding Staffs}">
        <CollectionViewSource.GroupDescriptions>
            <PropertyGroupDescription PropertyName="Model.Department" />
        </CollectionViewSource.GroupDescriptions>
    </CollectionViewSource>
</UserControl.Resources>
```

２．コレクションをバインド

```xml
<ItemsControl ItemsSource="{Binding Source={StaticResource StaffCollectionKey}}">
  (略)
</ItemsControl>
```

３．ヘッダーのスタイルを定義

```xml
<ItemsControl.GroupStyle>
    <GroupStyle>
        <GroupStyle.HeaderTemplate>
            <DataTemplate>
                <Border Margin="1 0 1 4"
                        Background="DimGray">
                    <TextBlock Text="{Binding Name}"
                               Foreground="Snow"/>
                </Border>
            </DataTemplate>
        </GroupStyle.HeaderTemplate>
    </GroupStyle>
</ItemsControl.GroupStyle>

```


上記３ステップでグループ化表示ができます。（WPFステキ！）

![キャプチャ3.PNG](https://qiita-image-store.s3.amazonaws.com/0/77732/38226c9a-1e3a-b870-fe60-5b6803d175b4.png)


## 本題

無事にグループ化表示できたわけですが、スクロールしたときにグループヘッダーも同時に移動してしまう欠点があります。

![キャプチャ4.PNG](https://qiita-image-store.s3.amazonaws.com/0/77732/7bce6841-5883-5882-d40a-9ac2688d4f03.png)
↑「1010 スタッフ３」の所属がわからない、の図

今回はこの欠点を克服するために、スクロール時に常にグループヘッダが表示されるようにカスタマイズを行います。


## 実装方法

基本的には[この記事](http://blog.qarim.net/2013/08/16/wpf-persistent-group-headers-for-the-listbox-control/)と同じように、Adornerを利用します。参照元では添付プロパティで実装してますが、ここではBehaviorとして実装します。また、ListBoxにしか対応できない（ItemsControl内にScrollViewerが存在すること前提になっている）ため、ListBox以外のItemsControlにも対応できるように修正しました。


```csharp

    /// <summary>
    /// GroupItemのヘッダを固定表示するBehavior
    /// </summary>
    public class GroupHeaderFrozenBehavior : Behavior<ItemsControl>
    {
        private static readonly Dictionary<GroupItem, WeakReference<HeaderAdorner>> _CurrentGroupItem = new Dictionary<GroupItem, WeakReference<HeaderAdorner>>();

        #region HeaderTemplate依存関係プロパティ
        public DataTemplate HeaderTemplate
        {
            get { return (DataTemplate)GetValue(HeaderTemplateProperty); }
            set { SetValue(HeaderTemplateProperty, value); }
        }

        // Using a DependencyProperty as the backing store for HeaderTemplate.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty HeaderTemplateProperty =
            DependencyProperty.Register("HeaderTemplate", typeof(DataTemplate), typeof(GroupHeaderFrozenBehavior), new PropertyMetadata(null)); 
        #endregion


        protected override void OnAttached()
        {
            base.OnAttached();

            AssociatedObject.Loaded += AssociatedObject_Loaded;
            AssociatedObject.Unloaded += AssociatedObject_Unloaded;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();

            AssociatedObject.Loaded -= AssociatedObject_Loaded;
            AssociatedObject.Unloaded -= AssociatedObject_Unloaded;
        }

        private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
        {
            SetScrollChangedEvent(true);
        }

        private void AssociatedObject_Unloaded(object sender, RoutedEventArgs e)
        {
            SetScrollChangedEvent(false);
        }

        private void SetScrollChangedEvent(bool add)
        {
            ScrollViewer scrollViewer;

            if(AssociatedObject is ListBox)
            {
                // リストボックス内のScrollViewerを探す
                scrollViewer = AssociatedObject.FindChild<ScrollViewer>();
            }
            else
            {
                // 親方向にScrollViewerを探す
                scrollViewer = AssociatedObject.FindParent<ScrollViewer>();
            }

            if (scrollViewer != null)
            {
                if(add)
                {
                    scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
                }
                else
                {
                    scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
                }
            }
        }

        private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
        {
            var scrollViewer = (ScrollViewer)sender;

            // ScrollViewerの描画サイズ
            var scrollViewerRectangle = new Rect(new Point(0, 0), scrollViewer.RenderSize);

            // ItemsControlのGroupItem
            foreach(var containerItem in AssociatedObject.ItemContainerGenerator.Items)
            {
                // GroupItemコントロールを取得
                var groupItemContainer = AssociatedObject.ItemContainerGenerator.ContainerFromItem(containerItem) as GroupItem;
                if(groupItemContainer==null)
                {
                    Debug.WriteLine("Failed: get groupItemContainer");
                    return;
                }

                // ScrollViewerを基準とした描画位置を計算
                var transform = groupItemContainer.TransformToAncestor(scrollViewer);
                var groupItemRect = transform.TransformBounds(new Rect(new Point(0, 0), groupItemContainer.RenderSize));

                // ScrollViewerとGroupItemの重なりを確認
                var intersectRect = Rect.Intersect(scrollViewerRectangle, groupItemRect);

                // ヘッダー固定表示用のAdornerを表示する必要があるかどうか
                var needDisplayAdorner = true;

                // ScrollViewerの描画エリア内にGroupItemが配置されている
                needDisplayAdorner &= intersectRect != Rect.Empty;
                
                // かつ、GroupItemの上端がScrollViewerの上端よりも上に存在
                needDisplayAdorner &= groupItemRect.Top <= 0;

                // GroupItemのAdornerLayerを取得
                var adornerLayer = AdornerLayer.GetAdornerLayer(groupItemContainer);
                if(adornerLayer==null)
                {
                    Debug.WriteLine("Failed: get adornerLayer of GroupItem");
                    return;
                }

                // Adornerの表示が必要な場合
                if(needDisplayAdorner)
                {
                    // すでにAdornerを作成している場合はその位置を更新するだけ
                    var headerAdorner = GetAdorner(groupItemContainer);
                    if (headerAdorner != null)
                    {
                        headerAdorner.UpdateLocation(groupItemRect.Top);
                        return;
                    }

                    // 未作成の場合は新規にAdornerを作成し、Dictionaryに加える
                    var adorner = new HeaderAdorner(groupItemContainer)
                    {
                        DataContext = containerItem,
                        HeaderTemplate = HeaderTemplate,
                        Top = Math.Abs(groupItemRect.Top)
                    };
                    adornerLayer.Add(adorner);

                    _CurrentGroupItem.Add(groupItemContainer, new WeakReference<HeaderAdorner>(adorner));
                }
                // Adornerの表示が不要な場合
                else
                {
                    // AdornerをAdornerLayerとDictionaryから除去
                    var adorner = GetAdorner(groupItemContainer);
                    if (adorner != null)
                    {
                        adornerLayer.Remove(adorner);
                        _CurrentGroupItem.Remove(groupItemContainer);
                    }
                }
            }

        }


        private HeaderAdorner GetAdorner(GroupItem container)
        {
            if (_CurrentGroupItem.ContainsKey(container))
            {
                HeaderAdorner adorner;
                if (_CurrentGroupItem[container].TryGetTarget(out adorner))
                {
                    return adorner;
                }
            }

            return null;
        }
    }

```

利用するときは、下記のようにItemsControlにBehaviorを設定＆GroupItemのHeaderTemplateを設定します。グループヘッダーはDataTemplateとしてリソース化しておくと管理しやすくなります。

```xml
<UserControl x:Class="ControlsShowcase.Views.GroupItemTabView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             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:local="clr-namespace:ControlsShowcase.Views"
             xmlns:vm="clr-namespace:ControlsShowcase.ViewModels"
             xmlns:b="clr-namespace:ControlsShowcase.Behaviors"
             mc:Ignorable="d" 
             Foreground="Black"
             d:DesignHeight="300" d:DesignWidth="300">

    <UserControl.Resources>
        <CollectionViewSource x:Key="StaffCollectionKey"
                              Source="{Binding Staffs}">
            <CollectionViewSource.GroupDescriptions>
                <PropertyGroupDescription PropertyName="Model.Department" />
            </CollectionViewSource.GroupDescriptions>
        </CollectionViewSource>

        <DataTemplate x:Key="GroupItemHeaderTemplateKey">
            <Border Margin="2"
                    Padding="4 2"
                    Background="DimGray">
                <TextBlock Text="{Binding Name}"
                           Foreground="Snow"/>
            </Border>

        </DataTemplate>
    </UserControl.Resources>
    
    <UserControl.DataContext>
        <vm:GroupItemTabViewModel />
    </UserControl.DataContext>
    
    <Grid>
        <ScrollViewer>
            <ItemsControl ItemsSource="{Binding Source={StaticResource StaffCollectionKey}}">
                <i:Interaction.Behaviors>
                    <b:GroupHeaderFrozenBehavior HeaderTemplate="{StaticResource GroupItemHeaderTemplateKey}" />
                </i:Interaction.Behaviors>
                
                <ItemsControl.GroupStyle>
                    <GroupStyle HeaderTemplate="{StaticResource GroupItemHeaderTemplateKey}">
                    </GroupStyle>
                </ItemsControl.GroupStyle>
                
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Grid Margin="4 1">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="60" />
                                <ColumnDefinition />
                            </Grid.ColumnDefinitions>

                            <TextBlock Text="{Binding Model.Id}" />

                            <TextBlock Grid.Column="1"
                                       Text="{Binding Model.Name}" />
                        </Grid>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </ScrollViewer>

    </Grid>
</UserControl>

```

これでめでたくグループヘッダーの表示をスクロール時に固定することができました！

![groupitem2.gif](https://qiita-image-store.s3.amazonaws.com/0/77732/b578eff1-10cb-7d7c-e453-2d774c19ef5e.gif)

ソースは[こちら](https://github.com/ambleside138/ControlsShowcase)に上げています。

## 参考

http://blog.qarim.net/2013/08/16/wpf-persistent-group-headers-for-the-listbox-control/