基本のおさらい
まずは基本的な部分の確認です。
1.CollectionViewSourceを用意
2.ItemsControlでItemsSourceに1で用意したCollectionViewSourceをバインド
3.スタイルでグループヘッダーの見た目を定義
具体的には
1.リソースにCollectionViewSourceを定義
PropertyGroupDescriptionでグループ化したいプロパティ名称を指定します。
<UserControl.Resources>
<CollectionViewSource x:Key="StaffCollectionKey"
Source="{Binding Staffs}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Model.Department" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</UserControl.Resources>
2.コレクションをバインド
<ItemsControl ItemsSource="{Binding Source={StaticResource StaffCollectionKey}}">
(略)
</ItemsControl>
3.ヘッダーのスタイルを定義
<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>
上記3ステップでグループ化表示ができます。(WPFステキ!)
本題
無事にグループ化表示できたわけですが、スクロールしたときにグループヘッダーも同時に移動してしまう欠点があります。
今回はこの欠点を克服するために、スクロール時に常にグループヘッダが表示されるようにカスタマイズを行います。
実装方法
基本的にはこの記事と同じように、Adornerを利用します。参照元では添付プロパティで実装してますが、ここではBehaviorとして実装します。また、ListBoxにしか対応できない(ItemsControl内にScrollViewerが存在すること前提になっている)ため、ListBox以外のItemsControlにも対応できるように修正しました。
/// <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としてリソース化しておくと管理しやすくなります。
<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>
これでめでたくグループヘッダーの表示をスクロール時に固定することができました!
ソースはこちらに上げています。
参考