はじめに
前回の記事ではページネーションのスタイルの編集、ViewModel で管理するところまで実装しました。
今回は SQLite に格納されたデータを制御してページネーションに反映できるようにしていきます。
ライブラリのインストール
必要なライブラリをインストールします。
[ツール] > [コマンドライン] > [開発用 PowerShell] より PowerShell を起動します。
以下のコマンドを実行します。
dotnet add package Dapper `
&& dotnet add package Microsoft.Data.SqlClient `
&& dotnet add package Microsoft.Data.Sqlite
IUsersRepository と Users を修正する
現在のIUsersRepository<T>
には全てのデータを取得するメソッドしかなかったので、pageNumber
とpageSize
を指定できるメソッドを追加します。
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MVVM.DataGridPagination.Repositories
{
public interface IUsersRepository<T>
{
Task<IEnumerable<T>> GetUsersAsync();
+ Task<IEnumerable<T>> GetPaginateAsync(int pageNumber, int pageSize); // 追加
}
}
実装クラス側で SQL 文等を記述します。
using Dapper;
using MVVM.DataGridPagination.Entities;
using MVVM.DataGridPagination.Infrastructure.Helpers;
using MVVM.DataGridPagination.Repositories;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MVVM.DataGridPagination.Infrastructure.Implements
{
public sealed class Users : IUsersRepository<UserEntity>
{
/// <summary>
/// ユーザーをページネーションして非同期的に取得する
/// </summary>
/// <param name="pageNumber">取得するページ番号(1から始まる)</param>
/// <param name="pageSize">1ページあたりに取得するユーザー数</param>
/// <returns>ユーザーエンティティ</returns>
public async Task<IEnumerable<UserEntity>> GetPaginateAsync(int pageNumber, int pageSize)
{
var parameters = new DynamicParameters();
// @PageSize や @Offset: パラメータ(プレースホルダー)
// LIMIT @PageSize: 指定された pageSize 分の数のデータを返す
// OFFSET @Offset: 指定された offset 分のデ ータを飛ばす
// ※pageSize は常に500が渡ってくる
var sql = @"SELECT * FROM Users ORDER BY Id LIMIT @PageSize OFFSET @Offset";
// データを取得する位置を計算する
// 例: (2 - 1) * 500 = 500;
// → 500行分のデータを飛ばして501行目からデータを取得する
var offset = (pageNumber - 1) * pageSize;
// パラメータにバインドする値を指定
parameters.Add("@PageSize", pageSize);
parameters.Add("@Offset", offset);
// クエリ文を実行
return await SqlHelper.Query<UserEntity>(sql, parameters);
}
/// <summary>
/// 全てのユーザーを非同期的に取得する
/// </summary>
/// <returns>ユーザーエンティティ</returns>
public async Task<IEnumerable<UserEntity>> GetUsersAsync()
{
var sql = @"SELECT * FROM Users";
return await SqlHelper.Query<UserEntity>(sql);
}
}
}
データの取得範囲の指定はコメントに書いてあるとおりです。LIMIT
とOFFSET
を使用しています。
SQL ヘルパークラスの作成
次に、SQL 文を実行する処理をヘルパークラスに切り出します。
今回はデータを取得する処理のみ記述しています。
using Dapper;
using Microsoft.Data.SqlClient;
using Microsoft.Data.Sqlite;
using MVVM.DataGridPagination.Infrastructure.FileIO;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Threading.Tasks;
namespace MVVM.DataGridPagination.Infrastructure.Helpers
{
/// <summary>
/// データベース接続用のヘルパークラス
/// </summary>
internal static class SqlHelper
{
/// <summary>
/// 接続文字列
/// </summary>
internal static string _connectionString;
/// <summary>
/// static なコンストラクター
/// このクラスの _connectionString などのメンバー変数にアクセスしようとしたときに最初に実行される
/// </summary>
static SqlHelper()
{
var sourceDirectory = ConfigurationManager.AppSettings.Get("SQLiteFilePath");
if (!DirectoryAndFileHelper.IsExistsDirectory(sourceDirectory))
{
throw new FileNotFoundException("SQLiteが格納されたディレクトリが存在しません");
}
var sourceDilePath = Path.Combine(sourceDirectory, "DataGridSearch.db");
if (!DirectoryAndFileHelper.IsExistsFile(sourceDilePath))
{
throw new FileNotFoundException("SQLiteのファイルが存在しません");
}
var builder = new SqlConnectionStringBuilder();
builder.DataSource = sourceDilePath;
// 接続文字列を生成
_connectionString = builder.ToString();
}
internal static async Task<IEnumerable<T>> Query<T>(string sql)
{
return await Query<T>(sql, null);
}
internal static async Task<IEnumerable<T>> ParamQuery<T>(string sql, object param)
{
return await Query<T>(sql, param);
}
internal static async Task<IEnumerable<T>> Query<T>(string sql, object param)
{
using (var connection = new SqliteConnection(_connectionString))
{
// IEnumerable を List に変換する
// Dapperの機能でSQL文の実行結果を T に指定するエンティティにマッピングをする
// SELECTのカラム数とエンティティの引数の値、型など揃っていないとエラーになるので注意
// INFO: https://learn.microsoft.com/ja-jp/dotnet/framework/data/adonet/sql-server-data-type-mappings
return await connection.QueryAsync<T>(sql, param);
}
}
}
}
internal
修飾子はそのプロジェクト内(同じアセンブリ内)からのみアクセスを許可するものです。
internal
にしている理由ですが、このヘルパークラス内のメソッドは他のアセンブリ(ドメイン層やアプリケーション層)から直接使用することは想定していない為です。
今回のアプリケーションでは全て同じプロジェクト内MVVM.DataGridPagination.csproj
で作成していますが、本来であればInfrastructure
層のクラスライブラリを作成してデータベースへの札族や、ファイルの IO 処理などの処理を実装することでしょう。
その際にうまくinternal
を使用することでカプセル化を実現し、余 計な公開コードを作らない利点があります。
ViewModel の実装
先ほど作成したUsers
クラスをMainWindowViewModel
クラスに記述します。
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;
/// <summary>
/// 検索テキストが空かどうかを取得する
/// </summary>
public bool IsTextEmpty => string.IsNullOrWhiteSpace(SearchText);
/// <summary>
/// 検索テキストを取得または設定する
/// </summary>
public string SearchText
{
get => _searchText;
set
{
if (SetProperty(ref _searchText, value))
{
RaisePropertyChanged(nameof(IsTextEmpty));
FilterItems();
}
}
}
private string _searchText;
private ObservableCollection<UserEntity> _originalMemberData;
/// <summary>
/// フィルタリングされたメンバーデータを取得または設定する
/// </summary>
public ObservableCollection<UserEntity> FilterMemberData
{
get => _filterMemberData;
set => SetProperty(ref _filterMemberData, value);
}
private ObservableCollection<UserEntity> _filterMemberData;
/// <summary>
/// タイトルを取得または設定する
/// </summary>
public string Title
{
get => _title;
set => SetProperty(ref _title, value);
}
private string _title = "Prism Application";
#endregion
#region コマンド
/// <summary>
/// 初期化を非同期で実行するコマンド
/// </summary>
public DelegateCommand InitializeAsyncCommand =>
_initializeAsyncCommand ?? (new DelegateCommand(async () => await InitializeAsync()));
private DelegateCommand _initializeAsyncCommand;
/// <summary>
/// アプリケーションを終了するコマンド
/// </summary>
public DelegateCommand AppCloseCommand =>
_appCloseCommand ?? (new DelegateCommand(() => Application.Current.Shutdown()));
private DelegateCommand _appCloseCommand;
/// <summary>
/// 検索を実行するコマンド
/// </summary>
public DelegateCommand SearchCommand =>
_searchCommand ?? (new DelegateCommand(OnSearch));
private DelegateCommand _searchCommand;
#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}");
}
}
/// <summary>
/// アイテムをフィルタリングする
/// </summary>
public void FilterItems()
{
if (string.IsNullOrWhiteSpace(SearchText)) // 検索文字列が空の場合はTrueを返す
{
FilterMemberData = _originalMemberData;
}
else
{
// 検索結果をTextBoxの文字列でフィルタリングする
var filterDate = _originalMemberData.Where(x => x.Name?.Contains(SearchText) == true);
// フィルタリング結果をObservableCollectionに変換する
FilterMemberData = filterDate == null ? new ObservableCollection<UserEntity>()
: new ObservableCollection<UserEntity>(filterDate);
}
}
/// <summary>
/// 検索を実行する
/// </summary>
private async void OnSearch()
{
try
{
var enumerableData = await _usersRepository.GetUsersAsync();
FilterMemberData = new ObservableCollection<UserEntity>(enumerableData);
// 絞り込み検索前のデータを保持する
_originalMemberData = FilterMemberData;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
}
}
ページネーションの動作確認
アプリケーションを起動してページネーションが機能していることを確認します。
使用したサンプルアプリケーションのリポジトリはこちらです。