情報応用演習Ⅰ(2024)

【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=ユーザー名1というクエリが来た場合にはユーザー名で 指定されたユーザーの記事だけを表示するようにしてみよう.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=ユーザー名という リンクが張られていることが分かるはずである(_).このリンクをクリックすると同じく記事一覧に遷移するが, クリックしたユーザー名の書いた記事だけが表示されていることが分かるはずである(_).

実行結果

  1. author(おーさー),英語で「著者」の意味 ↩︎

Last updated on 2024-06-18
Published on 2024-06-18

Powered by Hugo. Theme by TechDoc. Designed by Thingsym.