【T10a】簡易ブログソフトウェアの作成 Part.Ⅲ ~ 設定ファイルとクエリ文字列(4/5) プロジェクトタイプ (注意: 本文参照) プロジェクト名 T10a
ソリューション名 PIT10
注意 本ページの作業内容は 前のページまでの続き になっていることに注意せよ.先に前のページまでをすべて読み,指示されている作業を済ませてから本ページを読むこと. プロジェクトの作成作業については準備作業 を参照せよ. 10a-4. クエリ文字列 以前 に説明したように,URLのフォーマットはRFC3986 で定められており,
とくにWWWで使用されるURLは_ に示すパートを持っている.
URLのフォーマット
スキーム
://オーソリティ
パス
?クエリ
#フラグメント
例えばhttps://www.example.com/path/to/some/file.html?hoge=piyo&foo=bar#quux
のようなURLであれば,
各パートは_ のように対応する.
URLの各部の対応 パート 値 スキーム
https
オーソリティ
www.example.com
パス
/path/to/some/file.html
クエリ
?hoge=piyo&foo=bar
フラグメント
#quux
このうちスキーム,オーソリティ(≒ホスト名),パスの役割についてはチュートリアル【T3a】 で
説明済みである.これらはそれぞれアクセスする際のプロトコル(HTTP,HTTPSなど),アクセス先のホスト名,ホスト内のファイルパスを示している.
また読者の中にはフラグメントについても,その名称は知らずとも目にしたことがある者もいるだろう.
この#
から始まる文字列は,WWWではいわゆる「 ページ内リンク (≒アンカー)」でよく使用される.
クエリの役割は, アクセス先のリソース(ファイルなど)への入力データ である.
リソースに何らかの処理を依頼する場合は第03回 で見たように,POST
リクエストを用い,
リクエストボディにapplication/x-www-form-urlencoded
形式のデータを詰めて送る方法が一般的である.
しかし小さなデータであれば,リクエストのパスの部分に続けて?
から始まる
application/x-www-form-urlencoded
形式のデータを埋め込むことでも入力データを渡すことができる.
_ はそのようなリクエストの一例である.これは前述の
https://www.example.com/path/to/some/file.html?hoge=piyo&foo=bar#quux
に対するHTTPリクエストの例である.
クエリを持つHTTPリクエストの例
GET /path/to/some/file.html?hoge=piyo&foo=bar
HTTP/1.1
Host: www.example.com
このURLのクエリ
のパートはそのまま クエリ とか クエリ文字列 などと呼ばれ,
一般的なウェブアプリケーションではPOSTリクエストを使う以外のもう一つの入力データの渡し方である.
あまり意識することはないかもしれないが,これを最もよく目にするのは ウェブ検索サイト であろう.
試しにGoogle などで検索欄にキーワードを入力して検索してみよう.
検索結果のページのURLを観察してみるとよい.たとえば「 cat sleeping 」と入力して検索すると,
本校執筆時点では_ のようなURLに遷移した(_ ).
Googleで検索した際のURLの例
https://www.google.com/search?q=cat+sleeping
&sca_esv=9fc63cc73e9e59b8&source=hp&ei=2JHoZfypFJ6D2roPg-2j2AU&iflsig=ANes7DEAAAAAZeif6GvqbfbImLptwGi_M9dwcdjpW3Q_&ved=0ahUKEwj89azb_9-EAxWegVYBHYP2CFsQ4dUDCA8&uact=5&oq=cat+sleeping&gs_lp=Egdnd3Mtd2l6IgxjYXQgc2xlZXBpbmcyBRAAGIAEMgUQABiABDIFEAAYgAQyBRAAGIAEMgQQABgeMgQQABgeMgQQABgeMgQQABgeMgQQABgeMgQQABgeSLcGUPMBWPMBcAF4AJABAJgBWaABWaoBATG4AQPIAQD4AQL4AQGYAgKgAl-oAgrCAhAQABgDGI8BGOUCGOoCGIwDmAMEkgcBMqAHiwM&sclient=gws-wiz Googleで検索した際の例 ウェブアプリケーションに入力データを渡すこれらの2つの方法の決定的な違いは,クエリ文字列を用いた方法の場合は
入力データ付きのURLを共有できる点である .試しに_ のURLをウェブブラウザのアドレスバーに入力してみるとよい.
_ 右の検索結果のページに移動することができるはずである.
ここまでの例ではこのクエリ文字列で入力データを渡す方法を全く用いていなかった.
ASP.NET Core は,URLの ルーティング の仕組みを持つフレームワークである.ASP.NET Core ではアプリケーションに対して
アクセスされたURLのパス
の部分だけを見て,どのコントローラーのどのアクションを実行するかを決めている.
また入力データでさえもルーティングパラメーターの形でパスに埋め込まれている(_ ).
ASP.NET Coreのルーティング このルーティングの定義は Program.cs で定義されている.
現状のProgram.cs
におけるルーティングの定義の部分を_ に再掲する.
Program.csにおけるルーティングの定義 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
using KnzwTech.AspNetCore.ResourceBasedLocalization ;
using Microsoft.EntityFrameworkCore ;
using Microsoft.AspNetCore.Identity ;
using T10a.Data ;
using T10a.Models ;
AppContext . SetSwitch ( "Npgsql.EnableLegacyTimestampBehavior" , true );
var builder = WebApplication . CreateBuilder ( args );
// Add services to the container.
builder . Services . AddControllersWithViews ( opt => opt . EnableDefaultErrorMessagesFromResource ());
builder . Services . AddDbContext < T10aContext >(
opt => opt . UseNpgsql ( builder . Configuration . GetConnectionString ( nameof ( T10aContext ))));
builder . Services . AddIdentity < BlogUser , IdentityRole >()
. AddRoleManager < RoleManager < IdentityRole >>()
. AddDefaultTokenProviders ()
. AddEntityFrameworkStores < T10aContext >();
builder . Services . ConfigureApplicationCookie ( opt => {
opt . LoginPath = "/Accounts/Login" ;
opt . LogoutPath = "/Accounts/Logout" ;
opt . AccessDeniedPath = "/Accounts/AccessDenied" ;
});
builder . Services . AddTransient < IdentityDataSeeder >();
var app = builder . Build ();
// Configure the HTTP request pipeline.
if (! app . Environment . IsDevelopment ())
{
app . UseExceptionHandler ( "/Home/Error" );
}
app . UseStaticFiles ();
app . UseStatusCodePagesWithReExecute ( "/Home/AccessError/{0}" );
app . UseRouting ();
app . UseAuthentication ();
app . UseAuthorization ();
app . MapControllerRoute ( //
name : "default" , // URLルーティングの定義
pattern : "{controller=Articles}/{action=Index}/{id?}" ); //
IdentityDataSeeder . SeedData ( app );
app . Run ();
.MapControllerRoute()
メソッドがルーティングの方法を指示している部分である.
このアプリケーションではコントローラー名
/アクション名
/ルーティングパラメーター(省略可)
という
フォーマットでリクエストを受け取り,各部のデータに従ってどのコントローラーのどのアクションを実行するかを決定している.
ASP.NET Core では入力データはもっぱらPOSTリクエストの入力データか,もしくは事前定義された(この場合はパス
の一部である)
ルーティングパラメーターから受け取ることができるので,クエリ文字列を用いたデータの入力はそれほど頻繁に必要になることはない.
ただし,アクションごとに何らかの 動作モード のようなものを決定するためには,このようなクエリ文字列を用いるのが便利である.
本節ではクエリ文字列を受け取るアクションを定義する方法について学んでみよう.
ASP.NET Core のコントローラーのアクションでクエリ文字列を受け取るには, アクションメソッドに省略可能な(≒Null許容な)引数を追加すればよい .
試しに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
// GET: Articles
public async Task < IActionResult > Index ( string? greeting ) // ?greeting=xxx というクエリが来ることを想定
{
BlogUser ? currentUser = await _userManager . GetUserAsync ( User );
bool isAdminUser = await IsAdminUserAsync ( currentUser );
bool isLoggedIn = _signInManager . IsSignedIn ( User ) && currentUser != null ;
var articles = ( from a in _context . Articles
where ( a . PublicationState == PublicationStateType . Published
|| ( isLoggedIn && a . BlogUserId == currentUser !. Id )
|| isAdminUser )
orderby a . Modified descending
select a ). Include ( a => a . BlogUser );
ViewBag . CurrentUser = currentUser ;
ViewBag . IsAdminUser = isAdminUser ;
ViewBag . IsLoggedIn = isLoggedIn ;
ViewBag . Greeting = greeting ; // ↑のクエリ文字列を ViewBag に入れる
return View ( await articles . 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
@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保証のための回避策
}
< p > @ViewBag.Greeting</ p > @* ←単に表示する *@
@foreach(var a in Model)
{
< section class = "article" >
< h3 >< a asp-action = "Details" asp-route-id = "@a.ArticleId" > @a.Title</ a ></ h3 >
< p > @(a.BlogUser?.Nickname ?? a.BlogUser?.UserName) による投稿</ p >
< 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 (isLoggedIn)
{
< a asp-action = "Create" > 新規作成</ a >
}
ここまで書けたら実行してみよう.起動したらウェブブラウザのアドレスバーを編集してhttp://localhost:ポート番号
/Articles/Index?greeting=Hello!
に
アクセスしてみよう(_ ).すると記事一覧画面の上部にHello!
と表示されるはずである(_ ).
URLのHello!
の部分を適当に変えてアクセスしてみよう._ のようにURLの?greeting=この部分
が都度ページ上に表示される
はずである.
実行結果 このクエリ文字列をもう少し実用的に使用してみよう.例えばここまでのIndex
アクションでは,すべての「ユーザー」の書いたすべての「記事」が
一覧表示されていた.そこでIndex
アクションに対して,?author=ユーザー名
というクエリが来た場合にはユーザー名
で
指定されたユーザーの記事だけを表示するようにしてみよう.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
// GET: Articles
public async Task < IActionResult > Index ( string? author ) // ?author=xxx というクエリが来ることを想定
{
BlogUser ? currentUser = await _userManager . GetUserAsync ( User );
bool isAdminUser = await IsAdminUserAsync ( currentUser );
bool isLoggedIn = _signInManager . IsSignedIn ( User ) && currentUser != null ;
var articles = ( from a in _context . Articles
where ( a . PublicationState == PublicationStateType . Published
|| ( isLoggedIn && a . BlogUserId == currentUser !. Id )
|| isAdminUser )
&& ( author == null || a . BlogUser !. UserName == author ) // 記事の著者が author で指定したものかどうか
orderby a . Modified descending
select a ). Include ( a => a . BlogUser );
ViewBag . CurrentUser = currentUser ;
ViewBag . IsAdminUser = isAdminUser ;
ViewBag . IsLoggedIn = isLoggedIn ;
ViewBag . Greeting = greeting ; // ←削除
return View ( await articles . 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
@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保証のための回避策
}
< p > @ViewBag.Greeting</ p > @* ←削除 *@
@foreach(var a in Model)
{
< section class = "article" >
< h3 >< a asp-action = "Details" asp-route-id = "@a.ArticleId" > @a.Title</ a ></ h3 >
@* ↓ユーザー名に ?author=xxx のリンクを張る *@
< p >< a asp-action = "Index" asp-route-author = "@a.BlogUser?.UserName" > @(a.BlogUser?.Nickname ?? a.BlogUser?.UserName)</ a > による投稿</ p >
< 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 (isLoggedIn)
{
< a asp-action = "Create" > 新規作成</ a >
}
_ ではLINQで絞り込み条件を追加して(13行目),?author=ユーザー名
のクエリがあった場合は
そのユーザー名のユーザーが投稿した記事のみを取得するようにしている.
また,_ では「ユーザー名
による投稿」という部分のユーザー名
に,
?author=ユーザー名
のクエリを持つリンクを張っている.ASP.NET Core ではルーティングパラメーターと同様に
asp-route- タグヘルパーを用いることでクエリ文字列を持つリンクを生成することができる.このタグヘルパーは
asp-route-キー
="値
"
のようにして使用する.これによって?キー
=値
という
クエリ文字列が生成されてURLに付加される(キー
の部分がルーティングパラメーター名と一致している場合は,ルーティングパラメーターとして処理されたURLが生成される)._ の場合は,a 要素に
asp-route-author="@a.BlogUser.UserName"
というタグヘルパーの指定をしており,author
というクエリ文字列のキーに
その記事の所有者のユーザー名が渡されるようにしている.
ここまで書けたら実行してみよう._ に示す通り,記事の一覧の「ユーザー名
による投稿」の
ユーザー名
の部分にリンクが張られていることが分かるはずである.また,リンクにマウスオーバーすると,
Firefoxの場合はウィンドウ下部にリンク先のURLが表示される.それぞれ?author=ユーザー名
という
リンクが張られていることが分かるはずである(_ ).このリンクをクリックすると同じく記事一覧に遷移するが,
クリックしたユーザー名
の書いた記事だけが表示されていることが分かるはずである(_ ).
実行結果 Last updated on 2024-06-18 Published on 2024-06-18