【T10a】簡易ブログソフトウェアの作成 Part.Ⅲ ~ 設定ファイルとクエリ文字列(5/5) プロジェクトタイプ (注意: 本文参照) プロジェクト名 T10a
ソリューション名 PIT10
注意 本ページの作業内容は 前のページまでの続き になっていることに注意せよ.先に前のページまでをすべて読み,指示されている作業を済ませてから本ページを読むこと. プロジェクトの作成作業については準備作業 を参照せよ. 10a-5. ページング(ページネーション) 本節ではウェブアプリケーションでは一般的な処理であるページングについて学ぶ.ページング あるいは ページネーション とは
多数のデータの一覧を いくつかのページに分けて 表示することである.
本節ではテストデータとして80件前後の「記事」があらかじめ登録されている.現状のArticle
コントローラーの
Index
アクションではデフォルトではすべての記事が表示されてしまうため,非常に長いスクロールが発生してしまっていた(_ ).
Indexアクションの非常に長いスクロール このような多数のデータの表示は,ページナビゲーションとして不便であるだけでなくクライアントに転送するデータ量の肥大にもつながってしまう.
もしこのウェブブログサイトに1000件程度といった多数の「記事」データがあった場合,現状のIndex
アクションはそのすべてを
転送しようとしてしまい,これにはおそらく表示に長い時間がかかるはずである.このような場合の対処が
冒頭で説明した ページング である(あるいは ページネーション とも呼ばれる).ページングはウェブアプリでは基本的な処理の一つであり,たとえば前節で
例として挙げたウェブ検索サイトの検索結果のページでも,その結果全てを表示するのではなく先頭から数件ずつ結果を表示するようにしている (_ ).
ウェブ検索サイトの検索結果ページにおけるページングの例 ASP.NET Core でこのページングを実現するためには,LINQに対して.Skip()メソッド と
.Take()メソッド を使用すればよい.
これらは第02回 で説明のみ登場している.チュートリアル【T2c】の表2c-4-1 を_ に再掲する.
LINQで使用できる補助用のメソッド(一部 再掲) .Skip()
メソッドはLINQの結果セットの最初の何件かを読み飛ばすメソッドである.たとえばLINQの式
.Skip(5)
のように呼び出すと
元のLINQの式
の最初の5件が読み飛ばされて(≒削除されて)取得される..Take()
メソッドはLINQの結果セットの最初の何件かのみを
取得するメソッドである.たとえばLINQの式
.Take(10)
のように呼び出すと元のLINQの式
の最初の10件のみを取り出すことができる.
これらを組み合わせることでページングを実装することができる.
試しにこれらを使用してみよう.Articles
コントローラーのIndex
アクションに_ に示す内容を追記してみよう.
Articlesコントローラーの追記内容(Index) 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// GET: Articles
public async Task < IActionResult > Index ( string? author )
{
BlogUser ? currentUser = await _userManager . GetUserAsync ( User );
bool isAdminUser = await IsAdminUserAsync ( currentUser );
bool isLoggedIn = _signInManager . IsSignedIn ( User ) && currentUser != null ;
// ↓ var ではなく変数の型を明示的に IQueryable<T> と指定しておく必要がある.
IQueryable < Article > articles = ( from a in _context . Articles
where ( a . PublicationState == PublicationStateType . Published
|| ( isLoggedIn && a . BlogUserId == currentUser !. Id )
|| isAdminUser )
&& ( author == null || a . BlogUser !. UserName == author )
orderby a . Modified descending
select a ) /*.Include(a => a.BlogUser)*/ ; // .Include()の呼び出しを↓に移動する.
articles = articles . Skip ( 0 ). Take ( 5 ); // 0件スキップしてそこから5件を取得する.
ViewBag . CurrentUser = currentUser ;
ViewBag . IsAdminUser = isAdminUser ;
ViewBag . IsLoggedIn = isLoggedIn ;
return View ( await articles . Include ( a => a . BlogUser ). ToListAsync ()); // ここで.Include()を呼び出す
}
ここまで書けたら実行してみよう._ に示す通り,記事の一覧画面では
5件の記事のみが表示されていることが分かるはずである.また_ の
.Skip()
メソッドの引数を0から5, 10, 15,...と書き換えて実行してみよう(_ ).
そのつど表示される5件の内容が変化することが分かるはずである(_ ).
実行結果 このように「スキップする件数」と「何件表示するか」をプログラム的に調整することでページングを実現することが可能である.
それではこれらを使ってページングを実装してみよう.このためにまず「1ページに表示する件数」を決めておこう.
決め打ちでもよいが設定ファイルの利用 で学んだ,設定ファイルから読み出す方法を応用することにしよう.
appsettings.json および SiteSettings.cs にそれぞれ_ ,_ に示す内容を追記する.
appsettings.jsonの追記内容 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"Logging" : {
"LogLevel" : {
"Default" : "Information" ,
"Microsoft.AspNetCore" : "Warning"
}
},
"AllowedHosts" : "*" ,
"ConnectionStrings" : {
"T10aContext" : "Host=localhost;Port=5432;Username=t10a_user;Database=t10a_db;Password=Xbi!YzNm5"
},
"SiteSettings" : {
"SiteName" : "私のサイト" ,
"SiteOwnerName" : "名無しの権兵衛" ,
"SiteContactAddress" : "g-nanashi@thcu.ac.jp" , // ←ここカンマを追記するの忘れないように!
"NumberOfArticlesEachPage" : 5 // 1ページに何件の記事を表示するか
}
}
サイト設定のためのクラス 1
2
3
4
5
6
7
8
public class SiteSettings
{
public string SiteName { get ; set ; } = "MySite" ;
public string SiteOwnerName { get ; set ; } = "John Smith" ;
public string SiteContactAddress { get ; set ; } = "john@example.com" ;
public int NumberOfArticlesEachPage { get ; set ; } // 1ページに何件の記事を表示するか
}
これらの設定をArticles
コントローラーで読み出すことができるように, Controllers/ArticlesController.cs に
_ に示す内容を追記しよう.
Articlesコントローラーの追記内容(IConfigurationの追加) 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ArticlesController : Controller
{
private readonly T10aContext _context ;
private readonly UserManager < BlogUser > _userManager ;
private readonly SignInManager < BlogUser > _signInManager ;
private readonly IConfiguration _configuration ;
private SiteSettings _siteSettings ;
public ArticlesController ( T10aContext context , UserManager < BlogUser > userManager , SignInManager < BlogUser > signInManager , IConfiguration configuration )
{
_context = context ;
_userManager = userManager ;
_signInManager = signInManager ;
_configuration = configuration ;
_siteSettings = new SiteSettings ();
_configuration . GetSection ( nameof ( SiteSettings )). Bind ( _siteSettings );
}
// ..(略)..
このウェブログソフトウェアにおけるページングは,記事の一覧の 「何ページ目」を取得するかをIndex
アクションのクエリ文字列として受け取る ,
という形式をとることにしよう.ここでは?pageNumber=何ページ目
というクエリ文字列で指定することにする.
このためにArticles
コントローラーのIndex
アクションに_ に示す内容を追記しよう.
Articlesコントローラーの追記内容(Indexアクション(GETのみ)) 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// GET: Articles
public async Task < IActionResult > Index ( string? author , int? pageNumber ) // ?pageNumber=xxx というクエリが来ることを想定している.
{
BlogUser ? currentUser = await _userManager . GetUserAsync ( User );
bool isAdminUser = await IsAdminUserAsync ( currentUser );
bool isLoggedIn = _signInManager . IsSignedIn ( User ) && currentUser != null ;
IQueryable < Article > articles = ( from a in _context . Articles
where ( a . PublicationState == PublicationStateType . Published
|| ( isLoggedIn && a . BlogUserId == currentUser !. Id )
|| isAdminUser )
&& ( author == null || a . BlogUser !. UserName == author )
orderby a . Modified descending
select a ) /*.Include(a => a.BlogUser)*/ ; // .Include()の呼び出しを↓に移動する.
// pageNumberが未指定,すなわちnullのときは 1 として扱う.
pageNumber ??= 1 ;
// 全部で何ページ分あるかを計算する.
int pageCount = ( int ) Math . Ceiling ( await articles . CountAsync () / ( double ) _siteSettings . NumberOfArticlesEachPage );
if ( pageCount > 1 )
{
// 何件スキップするかを計算する.
int numberOfSkip = _siteSettings . NumberOfArticlesEachPage * Math . Max ( pageNumber . Value - 1 , 0 );
articles = articles . Skip ( numberOfSkip ). Take ( _siteSettings . NumberOfArticlesEachPage );
} //if
ViewBag . CurrentUser = currentUser ;
ViewBag . IsAdminUser = isAdminUser ;
ViewBag . IsLoggedIn = isLoggedIn ;
return View ( await articles . Include ( a => a . BlogUser ). ToListAsync ()); // ここで.Include()を呼び出す
}
21行目では全部で何ページ分になるかを計算し,全ページの合計が2ページ以上になるときのみページングの処理を行っている.
26行目でMath.Max()
メソッドを使用しているのは,pageNumber.Value - 1
の計算結果がマイナスにならないようにするための対処である
(pageNumber
にゼロや負数が入力された場合の対処).
ここまで書けたら実行してみよう.先ほどと同じように,記事の一覧画面では
5件の記事のみが表示されていることが分かるはずである.アドレスバーのURLの末尾を
/Articles/Index?pageNumber=何ページ目
のように
書き換えてアクセスしてみよう.何ページ目
の部分を1,2,3,...と書き換える都度,
表示される5件の内容が変化することが分かるはずである(_ ).
実行結果 このままでもよいが,ページ下部に「前へ」「次へ」のようなリンクを作ってよりアクセスしやすくしてみよう.
Articles
コントローラーのIndex
アクションとそのビューにそれぞれ_ ,
_ に示す内容を反映してみよう.
Articlesコントローラーの追記内容(Index) 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// GET: Articles
public async Task < IActionResult > Index ( string? author , int? pageNumber ) // ?pageNumber=xxx というクエリが来ることを想定している.
{
BlogUser ? currentUser = await _userManager . GetUserAsync ( User );
bool isAdminUser = await IsAdminUserAsync ( currentUser );
bool isLoggedIn = _signInManager . IsSignedIn ( User ) && currentUser != null ;
IQueryable < Article > articles = ( from a in _context . Articles
where ( a . PublicationState == PublicationStateType . Published
|| ( isLoggedIn && a . BlogUserId == currentUser !. Id )
|| isAdminUser )
&& ( author == null || a . BlogUser !. UserName == author )
orderby a . Modified descending
select a ) /*.Include(a => a.BlogUser)*/ ; // .Include()の呼び出しを↓に移動する.
pageNumber ??= 1 ;
int pageCount = ( int ) Math . Ceiling ( await articles . CountAsync () / ( double ) _siteSettings . NumberOfArticlesEachPage );
ViewBag . PageNumber = pageNumber ; // 現在のページ番号と
ViewBag . PageCount = pageCount ; // 最大ページ数を ViewBag に入れておく
ViewBag . ShowPages = false ; // ページのリンクを表示するかどうか(デフォルトでは非表示)
if ( pageCount > 1 )
{
int numberOfSkip = _siteSettings . NumberOfArticlesEachPage * Math . Max ( pageNumber . Value - 1 , 0 );
articles = articles . Skip ( numberOfSkip ). Take ( _siteSettings . NumberOfArticlesEachPage );
ViewBag . ShowPages = true ; // 全ページ数が1より大きいときだけページのリンクを表示する.
} //if
ViewBag . CurrentUser = currentUser ;
ViewBag . IsAdminUser = isAdminUser ;
ViewBag . IsLoggedIn = isLoggedIn ;
return View ( await articles . Include ( a => a . BlogUser ). ToListAsync ());
}
Views/Articles/Index.cshtmlの内容 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@model IEnumerable< T10a.Models.Article >
@{
ViewData["Title"] = "記事一覧";
var currentUser = (BlogUser?)ViewBag.CurrentUser;
var isAdminUser = (bool)ViewBag.IsAdminUser;
var isLoggedIn = (bool)ViewBag.IsLoggedIn;
if (Model is null) throw new ArgumentNullException(nameof(Model)); // ad-hoc! 非null保証のための回避策
}
@foreach(var a in Model)
{
< section class = "article" >
< h3 >< a asp-action = "Details" asp-route-id = "@a.ArticleId" > @a.Title</ a ></ h3 >
< p >< a asp-action = "Index" asp-route-author = "@a.BlogUser?.UserName" > @(a.BlogUser?.Nickname ?? a.BlogUser?.UserName)</ a > による投稿</ p > @* ←ユーザー名に ?author=xxx のリンクを張る *@
< pre > @a.Body</ pre >
@if (isLoggedIn && (isAdminUser || (currentUser != null && a.BlogUserId == currentUser.Id)))
{
< p > @Html.DisplayNameFor(_ => a.PublicationState):@Html.DisplayFor(_ => a.PublicationState)</ p >
< p >
< a asp-action = "Edit" asp-route-id = "@a.ArticleId" > 編集</ a >
| < a asp-action = "Delete" asp-route-id = "@a.ArticleId" > 削除</ a >
</ p >
}
</ section >
}
@if (ViewBag.ShowPages)
{
var pageNumber = (int)ViewBag.PageNumber; // 指定されているページと
var pageCount = (int)ViewBag.PageCount; // 全ページ数をとりだす.
// 「最初へ」のリンク(1ページ目への固定リンク)
< a asp-action = "Index" asp-route-pageNumber = "1" > 最初へ</ a >
// 「前へ」のリンク((現在指定されているページ - 1)ページ目へのリンク)
if (1 < pageNumber ) { < a asp-action = "Index" asp-route-pageNumber = "@(pageNumber - 1)" > 前へ</ a > }
else { < span > 前へ</ span > }
// 1~(全ページ数)までのリンクを生成する
for (int i = 1; i < = pageCount; ++i)
{
if ( i == pageNumber )
{
@i
}
else
{
< a asp-action = "Index" asp-route-pageNumber = "@i" > @i</ a >
}
< span > </ span >
}
// 「次へ」のリンク((現在指定されているページ + 1)ページ目へのリンク)
if (pageNumber < pageCount ){ < a asp-action = "Index" asp-route-pageNumber = "@(pageNumber + 1)" > 次へ</ a > }
else { < span > 次へ</ span > }
// 「最後へ」のリンク((全ページ数)ページ目への固定リンク)
< a asp-action = "Index" asp-route-pageNumber = "@pageCount" > 最後へ</ a >
< br />
}
@if (isLoggedIn)
{
< a asp-action = "Create" > 新規作成</ a >
}
_ のIndex
アクションでは,現在のページ番号と最大ページ数,それからページのリンクを表示するかどうかを
ViewBag
に格納している(22~24行目).このリンクは複数ページが存在するときだけ表示するものであるため,全ページ数が1より大きいときのみ
表示するように指示している(32行目).
_ の Index.cshtml では,ページングのためのリンクを生成している(31~48行目).
ここまで書けたら実行してみよう._ に示す通り,記事の一覧画面のページ下部に,
「最初へ」「前へ」「1 2 3...」「次へ」「最後へ」といったリンクが表示されていることが分かるはずである.
このリンクを使ってページを移動できることを確認しておこう(_ ,_ ).
実行結果 ここまでの動作を確認したら1つ目のチュートリアルは完了である.
次に進む前に,混乱を防ぐため Visual Studio のエディタをすべて閉じておこう.Visual Studio のいずれかのエディタのタブを右クリックして
「すべててのドキュメントを閉じる」をクリックすれば,エディタをすべて閉じることができる .
Last updated on 2024-06-18 Published on 2024-06-18