作成日:2023年4月2日
ASP.netでページング機能を実装するチュートリアルの記事は多いですか、どこか助長で拡張性に欠ける実装が多かったので、普段私が使用しているクラスの構成を共有します。
//ページング機能付き一覧ページ(ソート機能付き)
public interface IPaginationListViewModel<T> : IListViewRouteCondition
where T : class
{
//一覧の表示結果
public IPagedList<T> Results { get; set; }
}
//検索、ソート、ページングのパラメータ
public interface IListViewRouteCondition
{
//ソートするフィールド名
public string SortF { get; set; }
//ソートの方向
public string Dir { get; set; }
//表示したいページ
public int? Page { get; set; }
//一覧に検索項目がある場合はその値をDictionaryで取得できるメソッドを実装
public Dictionary<string, string> GetSearchKeyValues();
}
Expressionを使用しています。
SQLを発行する前に指定のプロパティ名からプロパティのオブジェクトを取得するコードをコンパイルするイメージです。
/// <summary>
/// プロパティ名で昇順ソート
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source"></param>
/// <param name="propertyName"></param>
/// <returns></returns>
public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string propertyName)
{
return source.OrderBy(ToLambda<T>(propertyName));
}
/// <summary>
/// プロパティ名で降順ソート
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source"></param>
/// <param name="propertyName"></param>
/// <returns></returns>
public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> source, string propertyName)
{
return source.OrderByDescending(ToLambda<T>(propertyName));
}
private static Expression<Func<T, object>> ToLambda<T>(string propertyName)
{
var parameter = Expression.Parameter(typeof(T));
var property = Expression.Property(parameter, propertyName);
var propAsObject = Expression.Convert(property, typeof(object));
return Expression.Lambda<Func<T, object>>(propAsObject, parameter);
}
デフォルト一覧表示行数10、ソート順のパラメータは"asc"、"desc"としています。
特に変わったことはしていませんが、
public abstract class PagedListProcessBase<T, T_Row> where T: IPaginationListViewModel<T_Row>
where T_Row : class
{
protected const int DEFAULT_PAGE_SIZE = 10;
protected const int DEFAULT_PAGE = 1;
protected const string DESC = "desc";//ソートの際に使用されるパラメータ(降順)
protected const string ASC = "asc";//ソートの際に使用されるパラメータ(昇順)
protected readonly AppDbContext _context;//EFのDbContext
public PagedListProcessBase(AppDbContext context)
{
_context = context;
}
public abstract Task<T> GetDataAsync(T vm);
/// <summary>
/// 基本クエリ
/// </summary>
/// <returns></returns>
protected virtual IQueryable<T_Row> GetBaseQuery() => _context.Set<T_Row>();
/// <summary>
/// ソート
/// </summary>
/// <param name="query"></param>
/// <param name="sortfield"></param>
/// <param name="descOrAsc"></param>
protected virtual void SetFieldSortQuery(ref IQueryable<T_Row> query, string sortfield, string descOrAsc)
{
if (String.IsNullOrEmpty(sortfield)) return;
if (descOrAsc == "desc")
{
query = query.OrderByDescending(sortfield);
}
else
{
query = query.OrderBy(sortfield);
}
}
}
IListViewSearchConditionで実装している検索パラメータ群をViewから取得し、ソート用のリンクを生成するコードです。
AchorTagHelperを継承していればRouteValuesプロパティにパラメータを追加するだけで、レンダリング後のhrefの値を編集できます。
ソートの順番やアイコンのタグなどは適宜調整してください。
public class SortTagHelper : AnchorTagHelper
{
//PagedListProcessBaseと同じ値を使用するので、どこかに静的な変数として保管しておくとBetter
const string DESC = "desc";
const string ASC = "asc";
public SortTagHelper(IHtmlGenerator generator ) : base(generator)
{
}
[HtmlAttributeName("asp-for")]
//現在のソート、ページング、検索条件
public IListViewRouteCondition SortAndSearch { get; set; }
/// <summary>
/// リンクを作成する対象のフィールド(プロパティ名)
/// </summary>
public string PropertyName { get; set; }
public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
//検索条件を設定
foreach (var searchVerb in SortAndSearch.GetSearchKeyValues())
{
if(!String.IsNullOrEmpty(searchVerb.Value))RouteValues.Add($"Con.{searchVerb.Key}",searchVerb.Value);
}
//ページング設定を設定
if (SortAndSearch.Page != null)
{
RouteValues.Add(nameof(SortAndSearch.Page), SortAndSearch.Page.ToString());
}
if (SortAndSearch.PageSize != null)
{
RouteValues.Add(nameof(SortAndSearch.PageSize), SortAndSearch.PageSize.ToString());
}
//ソート
var descOrAscClassName ="";
var sortmode = "";
//現在ソート対象のフィールドである場合、ソート順を参照して次回ソート順のリンクを作成
//ソートの順序:降順=>昇順=>ソート無し=>降順...
if (SortAndSearch.SortF == PropertyName)
{
descOrAscClassName = $"dir-{SortAndSearch.Dir}";//viewに反映されるクラス名
if (SortAndSearch.Dir == DESC)
{
sortmode = DESC;
RouteValues.Add(nameof(SortAndSearch.SortF), PropertyName);
RouteValues.Add(nameof(SortAndSearch.Dir), ASC);
}
else if (SortAndSearch.Dir == ASC)
{
sortmode = ASC;
}
}
//ソート対象でない場合降順ソートのリンクを作成
else
{
RouteValues.Add(nameof(SortAndSearch.SortF), PropertyName);
RouteValues.Add(nameof(SortAndSearch.Dir), DESC);
}
await base.ProcessAsync(context,output);
output.TagName = "a";
output.Attributes.Add("class", $"table-header {descOrAscClassName} text-nowrap sort-link");
//昇順、降順それぞれの場合に表示するアイコン
if (sortmode == ASC)
{
output.PostContent.SetHtmlContent($"<i class=\"bi bi-caret-down-fill\"></i>");
}
else if(sortmode == DESC)
{
output.PostContent.SetHtmlContent($"<i class=\"bi bi-caret-up-fill\"></i>");
}
}
}
/// <summary>
/// 顧客一覧検索条件
/// </summary>
public class GuestListSearchCondition
{
/// <summary>
/// 顧客名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 会社名
/// </summary>
public string CompanyName { get; set; }
}
IPaginationListViewModelを実装しておくだけでいいです。
※Guestsで定義しているのはEFでバインドされる対象のモデルです。
/// <summary>
/// 顧客一覧ViewModel(検索、ソート、ページングあり)
/// </summary>
public class GuestListViewModel : IPaginationListViewModel<Guests>
{
/// <summary>
/// 検索条件
/// </summary>
public GuestListSearchCondition Con { get; set; } = new GuestListSearchCondition();
/// <summary>
/// 一覧
/// </summary>
public IPagedList<Guests> Results { get; set; }
/// <summary>
/// ソートするカラム
/// </summary>
public string SortF { get; set; }
/// <summary>
/// ソートの方向
/// </summary>
public string Dir { get; set; }
/// <summary>
/// 現在のページ
/// </summary>
public int? Page { get; set; }
/// <summary>
/// ページサイズ
/// </summary>
public int? PageSize { get; set; }
/// <summary>
/// 検索条件をDictionaryで取得
/// </summary>
public Dictionary<string, string> GetSearchKeyValues()
{
return new Dictionary<string, string>
{
{ nameof(Con.Name),Con.Name}
};
}
}
PagedListProcessBaseを継承します。
検索、ソート、データ取得部分で特殊な動きを実現したい場合は検索、ソートメソッドをオーバーライドしてください。
public class GuestListProcess: PagedListProcessBase<GuestListViewModel, Guests>
{
public GuestListProcess(AppDbContext context):base(context)
{
}
/// <summary>
/// 表示データ取得
/// </summary>
/// <param name="vm"></param>
/// <returns></returns>
public async override Task<GuestListViewModel> GetDataAsync(GuestListViewModel vm)
{
//基本クエリ生成
var query = GetBaseQuery();
//ソート
SetFieldSortQuery(ref query, vm.SortF, vm.Dir);
//検索
SetSearchQuery(ref query, vm.Con);
//データ取得
vm.Results = await query.ToPagedListAsync(vm.Page ?? DEFAULT_PAGE, vm.PageSize ?? DEFAULT_PAGE_SIZE);
return vm;
}
/// <summary>
/// 検索
/// </summary>
/// <param name="query"></param>
/// <param name="con"></param>
private void SetSearchQuery(ref IQueryable<T_Guest> query, GuestListSearchCondition con)
{
//従業員名
query = query.WhereEx(!String.IsNullOrEmpty(con.Name), x => x.Name.Contains(con.Name));
//会社名
query = query.WhereEx(!String.IsNullOrEmpty(con.CompanyName), x => x.CompanyName.Contains(con.CompanyName));
}
}
/// <summary>
/// 従業員一覧
/// </summary>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> List(GuestListViewModel vm)
{
var proc = new GuestListProcess(_context);
return View(await proc.GetDataAsync(vm));
}
自作のタグヘルパーを利用し、asp-forにIListViewRouteConditionを実装しているModelを指定し、PropertyNameにはソート対象のプロパティ名を指定します。
<th scope="col">
<sort asp-for="@Model" property-name="@nameof(Guests.Name)">従業員名</sort>
</th>
今回は検索、ソート、ページング機能付きの一覧画面を実装しました。
この実装をしてから、一覧のViewとデータ取得処理部分の実装、保守工数が削減できました。
XpagedListを凌駕する便利なページングライブラリが出るまではこの方法で一覧画面を作っていく予定です。