【WPF】DataGridのデータをページネーションで制御する

【WPF】DataGridのデータをページネーションで制御する

はじめに

前回はコンストラクタ内で非同期の初期化処理を行う方法について解説しました。

今回の記事では、DataGrid に表示されたデータ数を自作したページネーションで制御する方法について見ていきます。

作成するサンプルアプリケーションです。

datagrid-pagination.gif

開発環境

  • Windows11
  • .NET8
  • Visual Studio 2022(Version 17.9.6)
  • Prism(9.0.537)

使用ライブラリ

  • Dapper(2.1.35)
  • MahApps.Metro.IconPacks.Material(5.0.0)
  • Microsoft.Data.SqlClient(5.2.2)
  • Microsoft.Data.Sqlite(8.0.8)
  • Microsoft.Xaml.Behaviors.Wpf(1.1.135)
  • System.Text.Json(8.0.4)

XAML を編集する

まずは既存の画面にページネーションのボタンとなる要素を配置します。

"MainWindow.xaml"
<Window x:Class="MVVM.DataGridPagination.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:Icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
        xmlns:bh="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:prism="http://prismlibrary.com/"
        Title="{Binding Title}"
        Width="1280"
        Height="800"
        prism:ViewModelLocator.AutoWireViewModel="True"
        AllowsTransparency="True"
        Background="Transparent"
        WindowStartupLocation="CenterScreen"
        WindowStyle="None">
    <bh:Interaction.Triggers>
        <bh:EventTrigger EventName="Loaded">
            <bh:InvokeCommandAction Command="{Binding InitializeAsyncCommand}" />
        </bh:EventTrigger>
    </bh:Interaction.Triggers>

    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="BoolToVis" />
    </Window.Resources>

    <!-- 省略 -->


    <!--#region ページネーション-->
    <StackPanel Grid.Row="2"
                Margin="0,16"
                HorizontalAlignment="Right"
                Orientation="Horizontal">
        <Button>
            <Icon:PackIconMaterial Kind="ChevronLeft"/>
        </Button>

        <!--  縦線  -->
        <Rectangle Width="1"
                Margin="3,7"
                Fill="#dee4ec" />

        <Button Content="1"  />
        <Button
                Content="2"
                Foreground="#ffffff"
                />
        <Button Content="3"  />

        <TextBlock Margin="10,0"
                VerticalAlignment="Center"
                FontSize="15"
                Text="..." />

        <Button Content="7" />
        <Button Content="8" />
        <Button Content="9" />

        <!--  縦線  -->
        <Rectangle Width="1"
                Margin="3,7"
                Fill="#dee4ec" />

        <Button >
            <Icon:PackIconMaterial Kind="ChevronRight" />
        </Button>
    </StackPanel>
    <!--#endregion-->


    <!-- 省略 -->
</Window>

現状のスタイルです。これから外観を整えていきます。

pagination-layout.png

リソースディクショナリにスタイルを切り出す

先程のボタンに共通のスタイルを適応させるためにResourcesフォルダ配下にPaginationButton.xamlというリソースディクショナリを作成し、以下のように記述します。

"PaginationButton.xaml"
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:Icon="http://metro.mahapps.com/winfx/xaml/iconpacks">

    <!--#region pagingButton-->
    <Style x:Key="pagingButton" TargetType="Button">
        <Setter Property="Background" Value="Transparent" />
        <Setter Property="Foreground" Value="#6c7682" />
        <Setter Property="FocusVisualStyle" Value="{x:Null}" />
        <Setter Property="FontSize" Value="13" />
        <Setter Property="Cursor" Value="Hand" />
        <Setter Property="Margin" Value="1,0" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Border Padding="16,8"
                            Background="{TemplateBinding Background}"
                            CornerRadius="5">
                        <ContentPresenter Margin="0,0,0,1"
                                          HorizontalAlignment="Center"
                                          VerticalAlignment="Center" />
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>

        <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="Background" Value="#3f51b5" />
                <Setter Property="Foreground" Value="#FFFFFF" />
            </Trigger>
        </Style.Triggers>
    </Style>
    <!--#endregion-->

    <!--#region pagingIcon-->
    <Style x:Key="pagingIcon" TargetType="Icon:PackIconMaterial">
        <Setter Property="Width" Value="10" />
        <Setter Property="Height" Value="10" />
        <Setter Property="VerticalAlignment" Value="Center" />
        <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="Foreground" Value="#ffffff" />
            </Trigger>
        </Style.Triggers>
    </Style>
    <!--#endregion-->

</ResourceDictionary>

作成したファイルをApp.xamlで読み込ませます。

"App.xaml"
<prism:PrismApplication x:Class="MVVM.DataGridPagination.App"
                        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                        xmlns:Icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
                        xmlns:local="clr-namespace:MVVM.DataGridPagination"
                        xmlns:prism="http://prismlibrary.com/">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Resources\TextBoxSearch.xaml" />
                <ResourceDictionary Source="Resources\ButtonStyle.xaml" />
                <ResourceDictionary Source="Resources\DataGridStyle.xaml" />
                <ResourceDictionary Source="Resources\CheckBoxStyle.xaml" />
                <!--  追加  -->
                <ResourceDictionary Source="Resources\PaginationButton.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</prism:PrismApplication>

そして最初にMainWindow.xamlに記述したページネーションのボタンなどにスタイルを適応させます。

"MainWindow.xaml"
<Window x:Class="MVVM.DataGridPagination.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:Icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
        xmlns:bh="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:prism="http://prismlibrary.com/"
        Title="{Binding Title}"
        Width="1280"
        Height="800"
        prism:ViewModelLocator.AutoWireViewModel="True"
        AllowsTransparency="True"
        Background="Transparent"
        WindowStartupLocation="CenterScreen"
        WindowStyle="None">
    <bh:Interaction.Triggers>
        <bh:EventTrigger EventName="Loaded">
            <bh:InvokeCommandAction Command="{Binding InitializeAsyncCommand}" />
        </bh:EventTrigger>
    </bh:Interaction.Triggers>

    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="BoolToVis" />
    </Window.Resources>

    <!-- 省略 -->


    <!--#region ページネーション-->
    <StackPanel Grid.Row="2"
            Margin="0,16"
            HorizontalAlignment="Right"
            Orientation="Horizontal">
        <Button Style="{StaticResource pagingButton}">
            <Icon:PackIconMaterial Kind="ChevronLeft" Style="{StaticResource gridButtonIcon}" />
        </Button>

        <!--  縦線  -->
        <Rectangle Width="1"
                Margin="3,7"
                Fill="#dee4ec" />

        <Button Content="1" Style="{StaticResource pagingButton}" />
        <Button Background="#3f51b5"
                Content="2"
                Foreground="#ffffff"
                Style="{StaticResource pagingButton}" />
        <Button Content="3" Style="{StaticResource pagingButton}" />

        <TextBlock Margin="10,0"
                VerticalAlignment="Center"
                FontSize="15"
                Text="..." />

        <Button Content="7" Style="{StaticResource pagingButton}" />
        <Button Content="8" Style="{StaticResource pagingButton}" />
        <Button Content="9" Style="{StaticResource pagingButton}" />

        <!--  縦線  -->
        <Rectangle Width="1"
                Margin="3,7"
                Fill="#dee4ec" />

        <Button Style="{StaticResource pagingButton}">
            <Icon:PackIconMaterial Kind="ChevronRight" Style="{StaticResource gridButtonIcon}" />
        </Button>
    </StackPanel>
    <!--#endregion-->


    <!-- 省略 -->
</Window>

現在のレイアウトを確認します。

pagination-style.gif

ボタンの外観やホバーしたときの動作などが整っていることが確認できました。

次は MVVM 構成で動的にページが切り替わるように実装していきます。

ページネーション ViewModel を作成する

動的にページネーションを表示するために専用の ViewModel を作成します。

この ViewModel では表示するページ数を管理したり、ボタンが押された際にページの移動などを実行します。

"ViewModels\PaginationViewModel.cs"
using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.ObjectModel;

namespace MVVM.DataGridPagination.ViewModels
{
    /// <summary>
    /// ページネーション機能を提供するViewModelクラス
    /// </summary>
    public sealed class PaginationViewModel : BindableBase
    {
        #region アクション

        /// <summary>
        /// ページが変更されたときに実行するアクション
        /// </summary>
        public event Action<int> PageChanged;

        #endregion

        #region プロパティ

        /// <summary>
        /// 現在のページ番号を取得または設定する
        /// </summary>
        public int CurrentPage
        {
            get => _currentPage;
            set
            {
                if (SetProperty(ref _currentPage, value))
                {
                    UpdatePageNumbers();

                    // アクションを通知(登録されたメソッドが実行される)
                    PageChanged?.Invoke(value);
                }
            }
        }
        private int _currentPage = 1;

        /// <summary>
        /// 総ページ数を取得または設定する
        /// </summary>
        public int TotalPages
        {
            get => _totalPages;
            set
            {
                if (SetProperty(ref _totalPages, value))
                {
                    UpdatePageNumbers();
                }
            }
        }
        private int _totalPages;

        /// <summary>
        /// 表示するページ数を取得または設定する
        /// </summary>
        public int DisplayedPageCount
        {
            get => _displayedPageCount;
            set => SetProperty(ref _displayedPageCount, value);
        }
        private int _displayedPageCount = 5;

        /// <summary>
        /// ページネーションデータを取得または設定する
        /// </summary>
        public ObservableCollection<object> PageNumbers
        {
            get => _pageNumbers;
            set => SetProperty(ref _pageNumbers, value);
        }
        private ObservableCollection<object> _pageNumbers = new ObservableCollection<object>();

        #endregion

        #region コマンド

        /// <summary>
        /// 前のページに移動するコマンド
        /// </summary>
        public DelegateCommand PreviousPageCommand
            => _previousPageCommand ?? (_previousPageCommand = new DelegateCommand(PreviousPage, CanPreviousPage)
                                                                                   .ObservesProperty(() => CurrentPage));
        private DelegateCommand _previousPageCommand;

        /// <summary>
        /// 次のページに移動するコマンド
        /// </summary>
        public DelegateCommand NextPageCommand
            => _nextPageCommand ?? (_nextPageCommand = new DelegateCommand(NextPage, CanNextPage)
                                                                          .ObservesProperty(() => CurrentPage));
        private DelegateCommand _nextPageCommand;

        /// <summary>
        /// 指定したページに移動するコマンド
        /// </summary>
        public DelegateCommand<object> GoToPageCommand
            => _goToPageCommand ?? (_goToPageCommand = new DelegateCommand<object>(ExecuteGoToPage));
        private DelegateCommand<object> _goToPageCommand;

        #endregion

        /// <summary>
        /// PaginationViewModelクラスの新しいインスタンスを初期化する
        /// </summary>
        public PaginationViewModel()
        {
            UpdatePageNumbers();
        }

        /// <summary>
        /// 指定したページに移動するコマンドを実行する
        /// </summary>
        /// <param name="parameter">移動先のページ番号</param>
        private void ExecuteGoToPage(object parameter)
        {
            if (parameter is int page)
            {
                GoToPage(page);
            }
        }

        /// <summary>
        /// 指定したページに移動する
        /// </summary>
        /// <param name="page">移動先のページ番号</param>
        private void GoToPage(int page) => CurrentPage = page;

        /// <summary>
        /// 次のページに移動可能かどうかを判定する
        /// </summary>
        /// <returns>次のページに移動可能な場合はtrue それ以外はfalse</returns>
        private bool CanNextPage() => CurrentPage < TotalPages;

        /// <summary>
        /// 次のページに移動する
        /// </summary>
        private void NextPage() => CurrentPage++;

        /// <summary>
        /// 前のページに移動する
        /// </summary>
        private void PreviousPage() => CurrentPage--;

        /// <summary>
        /// 前のページに移動可能かどうかを判定する
        /// </summary>
        /// <returns>前のページに移動可能な場合はtrue それ以外はfalse</returns>
        private bool CanPreviousPage() => CurrentPage > 1;

        /// <summary>
        /// ページ番号の表示を更新する
        /// </summary>
        private void UpdatePageNumbers()
        {
            PageNumbers.Clear(); // 既存の表示しているページ番号を消去する
            int middleItems = 5; // 中央に表示するページ番号の数

            // CurrentPage - middleItems / 2 は 現在のページを基準にページ番号を配置する
            // Math.Min と Math.Max を使用して 開始位置が1未満にならず かつ終了位置が総ページ数を超えないようにする
            int start = Math.Max(1, Math.Min(CurrentPage - middleItems / 2, TotalPages - middleItems + 1));

            // 開始位置から middleItems 分だけ進んだ位置を終了位置とする
            // Math.Min を使用して 終了位置が総ページ数を超えないようにする
            int end = Math.Min(start + middleItems - 1, TotalPages);

            // 開始部分の処理
            if (start > 1)
            {
                // 最初のページ(1)を常に表示する
                AddUniqueItem(1);

                // 開始位置が3以上の場合 省略記号(...)を追加する
                if (start > 2)
                {
                    AddUniqueItem("...");
                }
            }

            // 中央部分のページ番号を追加
            for (int i = start; i <= end; i++)
            {
                // 計算された start から end までのページ番号を順に追加する
                AddUniqueItem(i);
            }

            // 終了部分の処理
            if (end < TotalPages)
            {
                // 終了位置が最後から2ページ以上前の場合 省略記号(...)を追加する
                if (end < TotalPages - 1)
                {
                    AddUniqueItem("...");
                }

                // 最後のページを常に表示する
                AddUniqueItem(TotalPages);
            }
        }

        /// <summary>
        /// 重複したページ番号を追加しないようにする
        /// </summary>
        /// <param name="item">追加するページ番号または省略記号</param>
        private void AddUniqueItem(object item)
        {
            if (!PageNumbers.Contains(item))
            {
                PageNumbers.Add(item);
            }
        }
    }
}

ポイントとしては public event Action<int> PageChangedのアクションにMainWindowViewModel.cs側でイベントが通知された際の処理を登録しておき、その通知を元にデータベースから次のページのデータを取得するという点です。

そしてPageChangedの通知はPaginationViewModelクラスのCurrentPageプロパティ内で実行されています。

次にMainWindowViewModel.cs側の実装を見ていきます。

"MainWindowViewModel.cs"
using MVVM.DataGridPagination.Entities;
using MVVM.DataGridPagination.Repositories;
using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;

namespace MVVM.DataGridPagination.ViewModels
{
    /// <summary>
    /// メインウィンドウのViewModelクラス
    /// </summary>
    public class MainWindowViewModel : BindableBase
    {
        private readonly IUsersRepository<UserEntity> _usersRepository;

        #region プロパティ

        /// <summary>
        /// PaginationViewModelを取得または設定する
        /// </summary>
        public PaginationViewModel PaginationVM
        {
            get => _paginationVM;
            set => SetProperty(ref _paginationVM, value);
        }
        private PaginationViewModel _paginationVM;

        // 省略

        #endregion

        #region コマンド

        /// <summary>
        /// 初期化を非同期で実行するコマンド
        /// </summary>
        public DelegateCommand InitializeAsyncCommand =>
           _initializeAsyncCommand ?? (new DelegateCommand(async () => await InitializeAsync()));
        private DelegateCommand _initializeAsyncCommand;

        // 省略

        #endregion

        /// <summary>
        /// 1ページで表示するデータの数
        /// </summary>
        private const int PageSize = 500;

        /// <summary>
        /// MainWindowViewModelクラスの新しいインスタンスを初期化する
        /// </summary>
        /// <param name="usersRepository">Userリポジトリインターフェース</param>
        public MainWindowViewModel(IUsersRepository<UserEntity> usersRepository)
        {
            _usersRepository = usersRepository;

            PaginationVM = new PaginationViewModel();
            // PaginationViewModel側の Action で通知されたときに実行されるメソッドを登録
            PaginationVM.PageChanged += OnPageChange;
        }

        /// <summary>
        /// ページが変更されたときに呼び出されるメソッド
        /// </summary>
        /// <param name="page">新しいページ番号</param>
        private async void OnPageChange(int page)
        {
            await LoadPage(page);
        }

        /// <summary>
        /// 指定されたページのデータを読み込む
        /// </summary>
        /// <param name="page">読み込むページ番号</param>
        private async Task LoadPage(int page)
        {
            // 総レコード数を取得する
            var totalCount = await _usersRepository.GetUsersAsync();

            // ページネーション用の設定を行う
            PaginationVM.TotalPages = (int)Math.Ceiling((double)totalCount.Count() / PageSize);
            PaginationVM.CurrentPage = page;

            // 指定されたページのデータを取得する
            var enumerableData = await _usersRepository.GetPaginateAsync(page, PageSize);

            FilterMemberData = new ObservableCollection<UserEntity>(enumerableData);
        }

        /// <summary>
        /// 初期化を非同期で実行する
        /// </summary>
        /// <param name="pageNumber">初期ページ番号 デフォルトは1</param>
        private async Task InitializeAsync(int pageNumber = 1)
        {
            try
            {
                // 総レコード数を取得する
                var totalCount = await _usersRepository.GetUsersAsync();

                // ページネーション用の設定を行う
                PaginationVM.TotalPages = (int)Math.Ceiling((double)totalCount.Count() / PageSize);
                PaginationVM.CurrentPage = 1;

                // 指定されたページのデータを取得する
                var enumerableData = await _usersRepository.GetPaginateAsync(pageNumber, PageSize);

                FilterMemberData = new ObservableCollection<UserEntity>(enumerableData);
            }
            catch (Exception ex)
            {
                MessageBox.Show($"初期化中にエラーが発生しました: {ex.Message}");
            }
        }

        // 省略
    }
}

今回 DataGrid 内に表示するデータの数は500としています。

これらの ViewModel を画面に反映するために XAML 側も修正していきます。

次は動的に XAML のコントロールを切り替えるための実装をしていきます。

DataTemplateSelector で要素を切り替える

MainWindow.xamlでページネーションの外観を作成した箇所を以下のように修正します。

PaginationItemTemplateSelectorというクラスは作成していないのでコンパイルエラーが発生すると思いますが、後ほどそのクラスは実装します。

"MainWindow.xaml"

    <!-- 省略 -->

    <!--#region ページネーション-->
    <StackPanel Grid.Row="2"
            Margin="8"
            HorizontalAlignment="Right"
            Orientation="Horizontal">

        <StackPanel Orientation="Horizontal">

            <Button Command="{Binding PaginationVM.PreviousPageCommand}" Style="{StaticResource pagingButton}">
                <Icon:PackIconMaterial Kind="ChevronLeft" Style="{StaticResource pagingIcon}" />
            </Button>

            <!--  ItemTemplateSelector プロパティは DataTemplateSelector を指定できる  -->
           <ItemsControl ItemTemplateSelector="{StaticResource PaginationItemTemplateSelector}" ItemsSource="{Binding PaginationVM.PageNumbers}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <!--  DataTemplateSelector で選択された要素はこのStackPanelの子要素として表示される  -->
                        <StackPanel Orientation="Horizontal" />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
            </ItemsControl>

            <Button Command="{Binding PaginationVM.NextPageCommand}" Style="{StaticResource pagingButton}">
                <Icon:PackIconMaterial Kind="ChevronRight" Style="{StaticResource pagingIcon}" />
            </Button>

        </StackPanel>
    </StackPanel>
    <!--#endregion-->


    <!-- 省略 -->
</Window>

次にPaginationItemTemplateSelector.csファイルを作成します。

"Controls/PaginationItemTemplateSelector.cs"
using System.Windows;
using System.Windows.Controls;

namespace MVVM.DataGridPagination.Controls
{
    /// <summary>
    /// ページネーションアイテムのテンプレートを選択するためのセレクタークラス
    /// </summary>
    public sealed class PaginationItemTemplateSelector : DataTemplateSelector
    {
        /// <summary>
        /// 数字を表示するためのDataTemplateを取得または設定する
        /// </summary>
        public DataTemplate NumberTemplate { get; set; }

        /// <summary>
        /// 省略記号(...)を表示するためのDataTemplateを取得または設定する
        /// </summary>
        public DataTemplate EllipsisTemplate { get; set; }

        /// <summary>
        /// 指定されたアイテムに対して適切なDataTemplateを選択する
        /// </summary>
        /// <param name="item">テンプレートを選択するためのアイテム</param>
        /// <param name="container">ContentPresenter</param>
        /// <returns>選択されたDataTemplate</returns>
        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            if (item is string && (string)item == "...")
            {
                return EllipsisTemplate;
            }

            return NumberTemplate;
        }
    }
}

SelectTemplateをオーバーライドして任意の条件で表示する要素を切り替える実装をしています。

このメソッドはItemsControlで指定したItemsSourceプロパティでバインディングされているコレクションだけ実行されます。

例えば、初回実行時のページネーションは[1, 2, 3, 4, 5, "...", 10]となっているので7 回実行されます。

第一引数のobject itemにはItemsSource="{Binding PaginationVM.PageNumbers}"でバインディングされたコレクション内のページ番号が渡ってきます。

マルチデータバインディングでカレントページのボタンのスタイルを切替える

条件によって表示するコントロールを切替える実装をしました。

最後に現在のページだった場合にボタンのスタイルを変更する処理を実装します。

"MainWindow.xaml"
<Window x:Class="MVVM.DataGridPagination.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:Icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
        xmlns:bh="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:control="clr-namespace:MVVM.DataGridPagination.Controls"
        xmlns:converter="clr-namespace:MVVM.DataGridPagination.Converter"
        xmlns:prism="http://prismlibrary.com/"
        Title="{Binding Title}"
        Width="1280"
        Height="800"
        prism:ViewModelLocator.AutoWireViewModel="True"
        AllowsTransparency="True"
        Background="Transparent"
        WindowStartupLocation="CenterScreen"
        WindowStyle="None">
    <bh:Interaction.Triggers>
        <bh:EventTrigger EventName="Loaded">
            <bh:InvokeCommandAction Command="{Binding InitializeAsyncCommand}" />
        </bh:EventTrigger>
    </bh:Interaction.Triggers>

    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="BoolToVis" />

        <!-- 以下を追加 -->

        <!--#region ページネーション用コントロール-->
        <converter:EqualityConverter x:Key="EqualityConverter" />
        <control:PaginationItemTemplateSelector x:Key="PaginationItemTemplateSelector">
            <control:PaginationItemTemplateSelector.NumberTemplate>
                <DataTemplate>
                    <!--  AncestorType=ItemsControl で 親要素の ItemsControl の DataContextが設定される  -->
                    <!--
                        そのため CommandParameter="{Binding}" と Content="{Binding}" には
                        ItemsSource="{Binding PaginationVM.PageNumbers}" の値(ページ番号)が渡ってくる
                    -->
                    <Button Command="{Binding DataContext.PaginationVM.GoToPageCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
                            CommandParameter="{Binding}"
                            Content="{Binding}">
                        <Button.Style>
                            <Style BasedOn="{StaticResource pagingButton}" TargetType="Button">
                                <Style.Triggers>
                                    <!--  MultiBindingでわたってくる値をEqualityConverterで判定し、Trueだった場合ボタンの背景と文字の色が設定される  -->
                                    <DataTrigger Value="True">
                                        <DataTrigger.Binding>
                                            <!--  マルチバインドされた2つの値を比較し、Trueだった場合は背景色と文字色が設定される  -->
                                            <MultiBinding Converter="{StaticResource EqualityConverter}">
                                                <!--  DataContect内のItemを参照し、この場合はボタンのContentにバインドされているページ番号を取得  -->
                                                <Binding />
                                                <!--  親要素(ItemsControl)ページ番号を取得  -->
                                                <Binding Path="DataContext.PaginationVM.CurrentPage" RelativeSource="{RelativeSource AncestorType=ItemsControl}" />
                                            </MultiBinding>
                                        </DataTrigger.Binding>
                                        <Setter Property="Background" Value="#3f51b5" />
                                        <Setter Property="Foreground" Value="#ffffff" />
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </Button.Style>
                    </Button>
                </DataTemplate>
            </control:PaginationItemTemplateSelector.NumberTemplate>
            <control:PaginationItemTemplateSelector.EllipsisTemplate>
                <DataTemplate>
                    <TextBlock Margin="10,0"
                               VerticalAlignment="Center"
                               FontSize="15"
                               Text="..." />
                </DataTemplate>
            </control:PaginationItemTemplateSelector.EllipsisTemplate>
        </control:PaginationItemTemplateSelector>
        <!--#endregion-->
    </Window.Resources>

    <!-- 省略 -->

</Window>

次に新しくEqualityConverterというコンバーターを実装します。

"Converter/EqualityConverter.cs"
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace MVVM.DataGridPagination.Converter
{
    /// <summary>
    /// 2つの値の等価性を比較するためのマルチバリューコンバーター<br/>
    /// </summary>
    public class EqualityConverter : IMultiValueConverter
    {
        /// <summary>
        /// 2つの値を比較し等しいかどうかを判定する
        /// </summary>
        /// <param name="values">比較する値の配列。少なくとも2つの要素が必要</param>
        /// <param name="targetType">変換後の型。この実装では使用しない</param>
        /// <param name="parameter">追加のパラメータ。この実装では使用しない</param>
        /// <param name="culture">カルチャ情報。この実装では使用しない</param>
        /// <returns>
        /// 2つの値が等しい場合はtrue、そうでない場合はfalse<br/>
        /// 値の配列が無効な場合(要素数が2未満、または要素が未設定)も、falseを返す
        /// </returns>
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            /************************************************************************
             * values.Length
             * - MultiBindingから少なくとも2つの値が渡されることをチェック
             *
             * values[0] == DependencyProperty.UnsetValue
             * - 最初の値(通常はページ番号)が設定されていない、または無効な場合をチェックする
             * - DependencyProperty.UnsetValueは依存関係プロパティが明示的に設定されていない場合のデフォルト値で、この値が渡された場合比較が出来ないので false の条件とする
             *
             * values[1] == DependencyProperty.UnsetValue
             * - 2番目の値(通常は現在のページ番号)が設定されていない、または無効な場合をチェックする
             * - 動作は1番目の値のときと同じ
             ************************************************************************/
            if (values.Length < 2 || values[0] == DependencyProperty.UnsetValue || values[1] == DependencyProperty.UnsetValue)
            {
                /************************************************************************
                 * 条件に合致した場合に false を返すことで以下の状況を防ぐ
                 * - バインディングエラーによる例外の発生
                 * - 無効な値による誤った比較結果
                 * - 予期しない動作やビジュアルの不整合
                 ************************************************************************/
                return false;
            }

            // 2つの値が等しいかチェックする
            return Equals(values[0], values[1]);
        }

        /// <summary>
        /// 単一の値を複数の値に変換する。この実装では使用しない
        /// </summary>
        /// <param name="value">変換する値</param>
        /// <param name="targetTypes">目標とする型の配列</param>
        /// <param name="parameter">追加のパラメータ</param>
        /// <param name="culture">カルチャ情報</param>
        /// <returns>変換された値の配列</returns>
        /// <exception cref="NotImplementedException">このメソッドは実装されていない</exception>
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

このコンバーターの詳細な動作はコメントをに記載しましたが、簡単に言えばMultiBinding要素内でバインディングした値の比較をしています。

そしてDataTrigger Value="True"と指定しているので、

<DataTrigger.Binding>
    <!--  マルチバインドされた2つの値を比較し、Trueだった場合は背景色と文字色が設定される  -->
    <MultiBinding Converter="{StaticResource EqualityConverter}">
        <!--  DataContect内のItemを参照し、この場合はボタンのContentにバインドされているページ番号を取得  -->
        <Binding />
        <!--  親要素(ItemsControl)ページ番号を取得  -->
        <Binding Path="DataContext.PaginationVM.CurrentPage" RelativeSource="{RelativeSource AncestorType=ItemsControl}" />
    </MultiBinding>
</DataTrigger.Binding>

このDataTrigger.Binding内で指定されたMultiBindingEqualityConverterが返す値が True だった場合にのみ発火して、ボタンの背景色と文字色が変化します。

これまでの実装を確認してみます。

pagination-current-page-style.gif

現在のページのみボタンのスタイルが変わっていることが確認できました。

次回はページネーションボタンをクリックしたときに、取得するデータを切り替える実装をします。

参考