情報応用演習Ⅰ(2024)

【T9b】簡易ブログソフトウェアの作成 Part.Ⅱ ~ 記事関連機能の実装(5/11)

プロジェクトタイプ(注意: 本文参照)
プロジェクト名T9b
ソリューション名PIT9
注意
  • 本ページの作業内容は 前のページまでの続き になっていることに注意せよ.
    • 先に前のページまでをすべて読み,指示されている作業を済ませてから本ページを読むこと.
    • プロジェクトの作成作業については準備作業を参照せよ.

9b-5. 記事関連の操作のためのコントローラーの作成

つぎにこのエンティティの操作のためのコントローラーArticlesコントローラーを作成しよう. 前述の通り,このコントローラーのほとんどの機能は Entity Framework Core を用いた読み書き機能を持つ標準的なコントローラーであるため, Articlesコントローラーとビューは自動生成したものを手直しして用いることにしよう.

Controllersフォルダを右クリックし,「追加」→「コントローラー」をクリックし,「Entity Framework を使用したビューがある MVC コントローラー」を 選択する(__).各設定項目を以下のように設定し,そのほかの項目はデフォルトのまま「追加」ボタンをクリックする(_). コントローラー名はデフォルトのArticlesControllerのままでよい(_).

モデルクラス
ドロップダウンリストから Article クラスを選択する(表示上はArticle(T9b.Models)となっている)
DbContextクラス
ドロップダウンリストから T9bContext クラスを選択する(表示上はT9bContext(T9b.Data)となっている)
「Entity Framework を使用したビューがある MVC コントローラー」の作成

すると,以下の6個のファイルが追加されるはずである(_).

  • Controllers フォルダ
    • ArticlesController.cs
  • Views フォルダ
    • Articles フォルダ
      • Create.cshtml
      • Delete.cshtml
      • Details.cshtml
      • Edit.cshtml
      • Index.cshtml

これらはこのままでも動作するものもあるが,デフォルトではウェブログとしては意図する動作になっていない部分も多いので一つずつ手直ししていこう. このコントローラーではログイン済みかどうかや,どのロール(グループ)でログインしているかによって挙動が異なる. コントローラー内でこれらを判定するためには,AccountsコントローラーでそうしたようにUserManager<TUser>クラスと SignInManager<TUser>クラスのインスタンスが必要であるが,これはDIの仕組みを用いれば取得可能である. まず,ArticlesControllerクラスにprivateUserManager<TUser>型,SignInManager<TUser>型のフィールドを追加し, コンストラクタでそれらを初期化するようにしよう.また,今後の処理のためにいくつかのヘルパーメソッドも追加しておく. ソースコードの冒頭に_に示すusingディレクティブを追記する必要がある点に注意しよう.

必要なusingディレクティブ
1
2
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Authorization;

では Controllers/ArticlesController.cs に_に示す内容を追記しよう.

Articlesコントローラーの追記内容
 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
public class ArticlesController : Controller
{
    private readonly T9bContext _context;
    private readonly UserManager<BlogUser> _userManager;
    private readonly SignInManager<BlogUser> _signInManager;

    public ArticlesController(T9bContext context, UserManager<BlogUser> userManager, SignInManager<BlogUser> signInManager)
    {
        _context = context;
        _userManager = userManager;
        _signInManager = signInManager;
    }

    // 指定されたユーザーが管理者ユーザー(⇔ adminロールに所属しているかどうか)を
    // チェックするヘルパーメソッド
    private async Task<bool> IsAdminUserAsync(BlogUser? blogUser)
        => blogUser != null && await _userManager.IsInRoleAsync(blogUser, IdentityDataSeeder.AdminRoleName);

    // 指定されたユーザーが指定された記事を閲覧することができるかどうかを
    // チェックするヘルパーメソッド
    private async Task<bool> IsReadableAsync(BlogUser? blogUser, Article article) 
    {
        bool isAdminUser = await IsAdminUserAsync(blogUser);
        bool isLoggedIn = _signInManager.IsSignedIn(User) && blogUser != null;

        return article.PublicationState == PublicationStateType.Published
            || (isLoggedIn && (isAdminUser || article.BlogUserId == blogUser!.Id));
    }

    // 指定されたユーザーが指定された記事を編集もしくは削除することができるかどうかを
    // チェックするヘルパーメソッド
    private async Task<bool> IsModifiableAsync(BlogUser? blogUser, Article article) 
    {
        bool isAdminUser = await IsAdminUserAsync(blogUser);
        bool isLoggedIn = _signInManager.IsSignedIn(User) && blogUser != null;

        return isLoggedIn && (isAdminUser || article.BlogUserId == blogUser!.Id);
    }

    // GET: Articles
    public async Task<IActionResult> Index()
    {
        // ..(略)..

IsAdminUserAsync()メソッド(14~17行目)は,Accountsコントローラーで使用したものと同じものである.

IsReadableAsync()メソッド(19~28行目)は,引数blogUserで指定されたユーザーが,引数articleで指定された記事を閲覧することができるかどうかを チェックするヘルパーメソッドである.記事を閲覧できる条件は基本的に「公開状態」が「公開済み」であるときである. それ以外,つまり「公開状態」が「下書き」の記事の場合は,管理者ユーザーとしてログインしているならば 無条件で閲覧可能,通常ユーザーとしてログインしている場合は自身の作成した記事である場合に限り閲覧可能である(26~27行目). 閲覧可能であればこのメソッドは true を返す.

IsModifiableAsync()メソッド(30~38行目)は,引数blogUserで指定されたユーザーが,引数articleで指定された記事を編集もしくは削除することができるかどうかを チェックするヘルパーメソッドである.記事を編集・削除可能なのはログイン済みの場合で,管理者ユーザーの場合は 記事の作成者に関係なく編集・削除可能,通常ユーザーの場合は自分の作成した記事の場合の場合に限り編集・削除可能である(37行目). 編集もしくは削除可能であればこのメソッドは true を返す.

また,利便性のためAccountsコントローラーのログイン後のユーザー詳細画面(UserDetails)に, このArticleコントローラーのIndexアクションへのリンクを張っておこう. Views/Accounts/UserDetails.cshtml に_に示す内容を追記する.

Views/Accounts/UserDetails.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 BlogUser

@{
    ViewData["Title"] = "ユーザー詳細";

    if (Model is null) throw new ArgumentNullException(nameof(Model)); // ad-hoc! 非null保証のための回避策
}

<table>
    <tr><th>項目</th><th></th></tr>

    @* ユーザー名(Username)の表示 *@
    <tr>
        <td>@Html.DisplayNameFor(u => u.UserName)</td>
        <td>@Html.DisplayFor(u => u.UserName)</td>
    </tr>

    @* ニックネーム(Nickname)の表示 *@
    <tr>
        <td>@Html.DisplayNameFor(u => u.Nickname)</td>
        <td>@Html.DisplayFor(u => u.Nickname)</td>
    </tr>

    @* メールアドレス(Email)の表示 *@
    <tr>
        <td>@Html.DisplayNameFor(u => u.Email)</td>
        <td>@Html.DisplayFor(u => u.Email)</td>
    </tr>

    @* 登録日時(Registered)の表示 *@
    <tr>
        <td>@Html.DisplayNameFor(u => u.Registered)</td>
        <td>@Html.DisplayFor(u => u.Registered)</td>
    </tr>
</table>

<a asp-controller="Articles" asp-action="Index">全記事一覧</a>
| <a asp-action="EditUser" asp-route-id="@Model.UserName">編集</a>
| <a asp-action="DeleteUser" asp-route-id="@Model.UserName">退会(削除)</a>

さらに実行時に表示されるデフォルトのコントローラーとアクションをこのArticleコントローラーのIndexアクションに変更しておこう. 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 T9b.Data;
using T9b.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<T9bContext>(
    opt => opt.UseNpgsql(builder.Configuration.GetConnectionString(nameof(T9bContext))));

builder.Services.AddIdentity<BlogUser, IdentityRole>()
    .AddRoleManager<RoleManager<IdentityRole>>()
    .AddDefaultTokenProviders()
    .AddEntityFrameworkStores<T9bContext>();

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",
    pattern: "{controller=Articles}/{action=Index}/{id?}");

IdentityDataSeeder.SeedData(app);

app.Run();
Last updated on 2024-06-19
Published on 2024-06-19

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