【第二回】タスク管理アプリを作ろう

【第二回】タスク管理アプリを作ろう

目次

前回のおさらい
ナビゲーションメニューの日本語化
ログインや登録画面の日本語化
手動での日本語化
ログイン、登録画面の作成
修正
完成
最後に

前回のおさらい

前回はタスク管理アプリのプロジェクト作成からDBを作成して大枠は完成し、実行できるところまで確認出来ました
今回は画面の表示方法の変更など細かな修正を加えつつアプリの完成までをお届けしたいと思います

ナビゲーションメニューの日本語化

前回までの状態だとメニューが全て英語表記になってしまっているのでわかりづらくなってました
英語を全て日本語に変更することでより見やすく、わかりやすい作りにしていきます
実際にやり始めるとエラーが多発したり上手くいかなかったりと一番苦戦した部分でもあります
それでは初めていきます

まずはナビゲーションメニュー(Home,Privacy)を日本語化します
Views/Shared/_Layout.cshtmlを下記のように書きます

<ul class="navbar-nav flex-grow-1">
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">ホーム</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">プライバシー</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" asp-controller="TaskItems" asp-action="Index">📋 タスク管理</a>
    </li>
</ul>

続けてViews/Shared/_LoginPartial.cshtmlも下記のように書きます

@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">こんにちは、@User.Identity?.Name さん!</a>
    </li>
    <li class="nav-item">
        <form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Action("Index", "Home", new { area = "" })">
            <button type="submit" class="nav-link btn btn-link text-dark">ログアウト</button>
        </form>
    </li>
}
else
{
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">新規登録</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">ログイン</a>
    </li>
}
</ul>

これで実行してみると添付画像のように日本語になっていることが確認出来ました

ログインや登録画面の日本語化

次にログインや登録画面の日本語化を行います
方法としてはIdentityページをスキャフォールディングして日本語化を行っていきます
下記の手順でスキャフォールディングしていきます

1. ソリューションエクスプローラーでプロジェクト名(TaskManager)を右クリック
2. 追加 → スキャフォールディングされた新しい項目
3. Identity を選択して 追加
追加する下記の3ファイルを選択
Account\Login
Account\Register
Account\Logout
データコンテキストクラス は ApplicationDbContext を選択
ユーザークラス はそのまま(空欄でOK)
5. 追加 をクリック
追加されていることが確認出来たら下記の手順に進みます
※私の場合はこちらでエラーが発生しうまくスキャフォールディング出来なかったので手動で日本語化していきます

手動での日本語化

実はスキャフォールディングしなくとも既存のページを上書きしていくことで日本語化することも可能です
スキャフォールディングがうまくいかなかったので今回は手動で変更していきます

1. ソリューションエクスプローラーでプロジェクト(TaskManager)を右クリックします
  新しいフォルダーを追加して名前を「Areas」とする
2. 1で作成した「Areas」フォルダを右クリックしてフォルダー追加
  名前を「Identity」とする
3. 2で作成した「Identity」フォルダを右クリックしてフォルダー追加
  名前を「Pages」とする
4. 3で作成した「Pages」フォルダを右クリックしてビュー追加
 名前を「_ViewStart」として以下のコードを書きます

@{
    Layout = "/Views/Shared/_Layout.cshtml";
}

続けて「Pages」フォルダを右クリックしてフォルダー追加
名前を「Account」とする

ログイン、登録画面の作成

先ほど作成した「Account」フォルダを右クリックしてビューの追加
名前を「Login」とする
Login.cshtmlに下記のコードを書きます

@page
@model LoginModel

@{
    ViewData["Title"] = "ログイン";
}

<div class="container">
    <div class="row justify-content-center mt-5">
        <div class="col-md-6">
            <h1 class="text-center mb-4">@ViewData["Title"]</h1>
            <div class="card shadow">
                <div class="card-body p-4">
                    <form id="account" method="post">
                        <div asp-validation-summary="ModelOnly" class="text-danger mb-3" role="alert"></div>
                        
                        <div class="mb-3">
                            <label asp-for="Input.Email" class="form-label">メールアドレス</label>
                            <input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
                            <span asp-validation-for="Input.Email" class="text-danger"></span>
                        </div>
                        
                        <div class="mb-3">
                            <label asp-for="Input.Password" class="form-label">パスワード</label>
                            <input asp-for="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" placeholder="パスワード" />
                            <span asp-validation-for="Input.Password" class="text-danger"></span>
                        </div>
                        
                        <div class="mb-3 form-check">
                            <input class="form-check-input" asp-for="Input.RememberMe" />
                            <label asp-for="Input.RememberMe" class="form-check-label">
                                ログイン状態を保持する
                            </label>
                        </div>
                        
                        <div class="d-grid">
                            <button id="login-submit" type="submit" class="btn btn-primary btn-lg">ログイン</button>
                        </div>
                        
                        <div class="mt-3">
                            <p class="mb-2">
                                <a id="forgot-password" asp-page="./ForgotPassword">パスワードをお忘れですか?</a>
                            </p>
                            <p class="mb-0">
                                <a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">新規アカウント登録</a>
                            </p>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

Login.cshtml.csが自動で作成されているはずなので下記のコードを書きます

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;

namespace TaskManager.Areas.Identity.Pages.Account
{
    public class LoginModel : PageModel
    {
        private readonly SignInManager<IdentityUser> _signInManager;
        private readonly ILogger<LoginModel> _logger;

        public LoginModel(SignInManager<IdentityUser> signInManager, ILogger<LoginModel> logger)
        {
            _signInManager = signInManager;
            _logger = logger;
        }

        [BindProperty]
        public InputModel Input { get; set; }

        public IList<AuthenticationScheme> ExternalLogins { get; set; }

        public string ReturnUrl { get; set; }

        [TempData]
        public string ErrorMessage { get; set; }

        public class InputModel
        {
            [Required(ErrorMessage = "メールアドレスを入力してください")]
            [EmailAddress(ErrorMessage = "有効なメールアドレスを入力してください")]
            [Display(Name = "メールアドレス")]
            public string Email { get; set; }

            [Required(ErrorMessage = "パスワードを入力してください")]
            [DataType(DataType.Password)]
            [Display(Name = "パスワード")]
            public string Password { get; set; }

            [Display(Name = "ログイン状態を保持する")]
            public bool RememberMe { get; set; }
        }

        public async Task OnGetAsync(string returnUrl = null)
        {
            if (!string.IsNullOrEmpty(ErrorMessage))
            {
                ModelState.AddModelError(string.Empty, ErrorMessage);
            }

            returnUrl ??= Url.Content("~/");

            await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);

            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();

            ReturnUrl = returnUrl;
        }

        public async Task<IActionResult> OnPostAsync(string returnUrl = null)
        {
            returnUrl ??= Url.Content("~/");

            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();

            if (ModelState.IsValid)
            {
                var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
                
                if (result.Succeeded)
                {
                    _logger.LogInformation("ユーザーがログインしました。");
                    return LocalRedirect(returnUrl);
                }
                if (result.RequiresTwoFactor)
                {
                    return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
                }
                if (result.IsLockedOut)
                {
                    _logger.LogWarning("ユーザーアカウントがロックされました。");
                    return RedirectToPage("./Lockout");
                }
                else
                {
                    ModelState.AddModelError(string.Empty, "ログインに失敗しました。");
                    return Page();
                }
            }

            return Page();
        }
    }
}

続けて「Account」フォルダーにRazorページを追加
名前を「Register」として下記のコードを書きます

@page
@model RegisterModel
@{
    ViewData["Title"] = "新規登録";
}

<div class="container">
    <div class="row justify-content-center mt-5">
        <div class="col-md-6">
            <h1 class="text-center mb-4">@ViewData["Title"]</h1>
            <div class="card shadow">
                <div class="card-body p-4">
                    <form id="registerForm" asp-route-returnUrl="@Model.ReturnUrl" method="post">
                        <div asp-validation-summary="ModelOnly" class="text-danger mb-3" role="alert"></div>
                        
                        <div class="mb-3">
                            <label asp-for="Input.Email" class="form-label">メールアドレス</label>
                            <input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
                            <span asp-validation-for="Input.Email" class="text-danger"></span>
                        </div>
                        
                        <div class="mb-3">
                            <label asp-for="Input.Password" class="form-label">パスワード</label>
                            <input asp-for="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" placeholder="パスワード" />
                            <span asp-validation-for="Input.Password" class="text-danger"></span>
                        </div>
                        
                        <div class="mb-3">
                            <label asp-for="Input.ConfirmPassword" class="form-label">パスワード(確認)</label>
                            <input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="パスワード(確認)" />
                            <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
                        </div>
                        
                        <div class="d-grid">
                            <button id="registerSubmit" type="submit" class="btn btn-primary btn-lg">登録</button>
                        </div>
                        
                        <div class="mt-3">
                            <p class="mb-0">
                                すでにアカウントをお持ちですか? <a asp-page="./Login" asp-route-returnUrl="@Model.ReturnUrl">ログイン</a>
                            </p>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

Login.cshtml.cs同様にRegister.cshtml.csが自動で作成されているはずなので下記のコードを書きます

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
using System.ComponentModel.DataAnnotations;
using System.Text;
using System.Text.Encodings.Web;

namespace TaskManager.Areas.Identity.Pages.Account
{
    public class RegisterModel : PageModel
    {
        private readonly SignInManager<IdentityUser> _signInManager;
        private readonly UserManager<IdentityUser> _userManager;
        private readonly IUserStore<IdentityUser> _userStore;
        private readonly IUserEmailStore<IdentityUser> _emailStore;
        private readonly ILogger<RegisterModel> _logger;

        public RegisterModel(
            UserManager<IdentityUser> userManager,
            IUserStore<IdentityUser> userStore,
            SignInManager<IdentityUser> signInManager,
            ILogger<RegisterModel> logger)
        {
            _userManager = userManager;
            _userStore = userStore;
            _emailStore = GetEmailStore();
            _signInManager = signInManager;
            _logger = logger;
        }

        [BindProperty]
        public InputModel Input { get; set; }

        public string ReturnUrl { get; set; }

        public IList<AuthenticationScheme> ExternalLogins { get; set; }

        public class InputModel
        {
            [Required(ErrorMessage = "メールアドレスを入力してください")]
            [EmailAddress(ErrorMessage = "有効なメールアドレスを入力してください")]
            [Display(Name = "メールアドレス")]
            public string Email { get; set; }

            [Required(ErrorMessage = "パスワードを入力してください")]
            [StringLength(100, ErrorMessage = "{0}は{2}文字以上{1}文字以下で入力してください。", MinimumLength = 6)]
            [DataType(DataType.Password)]
            [Display(Name = "パスワード")]
            public string Password { get; set; }

            [DataType(DataType.Password)]
            [Display(Name = "パスワード(確認)")]
            [Compare("Password", ErrorMessage = "パスワードと確認用パスワードが一致しません。")]
            public string ConfirmPassword { get; set; }
        }

        public async Task OnGetAsync(string returnUrl = null)
        {
            ReturnUrl = returnUrl;
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
        }

        public async Task<IActionResult> OnPostAsync(string returnUrl = null)
        {
            returnUrl ??= Url.Content("~/");
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
            
            if (ModelState.IsValid)
            {
                var user = CreateUser();

                await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
                await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
                
                var result = await _userManager.CreateAsync(user, Input.Password);

                if (result.Succeeded)
                {
                    _logger.LogInformation("ユーザーが新しいアカウントを作成しました。");

                    await _signInManager.SignInAsync(user, isPersistent: false);
                    return LocalRedirect(returnUrl);
                }
                
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

            return Page();
        }

        private IdentityUser CreateUser()
        {
            try
            {
                return Activator.CreateInstance<IdentityUser>();
            }
            catch
            {
                throw new InvalidOperationException($"Can't create an instance of '{nameof(IdentityUser)}'. " +
                    $"Ensure that '{nameof(IdentityUser)}' is not an abstract class and has a parameterless constructor, or alternatively " +
                    $"override the register page in /Areas/Identity/Pages/Account/Register.cshtml");
            }
        }

        private IUserEmailStore<IdentityUser> GetEmailStore()
        {
            if (!_userManager.SupportsUserEmail)
            {
                throw new NotSupportedException("The default UI requires a user store with email support.");
            }
            return (IUserEmailStore<IdentityUser>)_userStore;
        }
    }
}

続けて「Account」フォルダーにRazorページを追加
名前を「Logout」として下記のコードを書きます

@page
@model LogoutModel
@{
    ViewData["Title"] = "ログアウト";
}

<div class="container">
    <div class="row justify-content-center mt-5">
        <div class="col-md-6">
            <h1 class="text-center mb-4">@ViewData["Title"]</h1>
            <div class="card shadow">
                <div class="card-body p-4 text-center">
                    <p class="mb-4">ログアウトしました。</p>
                    <a asp-page="./Login" class="btn btn-primary">ログインに戻る</a>
                </div>
            </div>
        </div>
    </div>
</div>

同様にRegister.cshtml.csが自動で作成されているはずなので下記のコードを書きます

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace TaskManager.Areas.Identity.Pages.Account
{
    public class LogoutModel : PageModel
    {
        private readonly SignInManager<IdentityUser> _signInManager;
        private readonly ILogger<LogoutModel> _logger;

        public LogoutModel(SignInManager<IdentityUser> signInManager, ILogger<LogoutModel> logger)
        {
            _signInManager = signInManager;
            _logger = logger;
        }

        public async Task<IActionResult> OnPost(string returnUrl = null)
        {
            await _signInManager.SignOutAsync();
            _logger.LogInformation("ユーザーがログアウトしました。");
            
            if (returnUrl != null)
            {
                return LocalRedirect(returnUrl);
            }
            else
            {
                return RedirectToPage();
            }
        }
    }
}

修正

これで実行すれば日本語化出来ているはずですが…実際に見てみると日本語化は出来ていますがボタンをクリックしても画面が各画面に遷移しなくなってしまいましたので、修正をかけていきます
Program.csに設定追加します
下記のコードに修正

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using TaskManager.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseNpgsql(connectionString));

builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => 
{
    options.SignIn.RequireConfirmedAccount = false;
    options.Password.RequireDigit = true;
    options.Password.RequireLowercase = true;
    options.Password.RequireUppercase = false;
    options.Password.RequireNonAlphanumeric = false;
    options.Password.RequiredLength = 6;
})
.AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddControllersWithViews();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "areas",
    pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.MapRazorPages();  // ← これ重要!

app.Run();

実行してみますがURLがおかしくなっておりエラーが出てしまいます
次に_LoginPartial.cshtmlを下記のコードに修正します

@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">こんにちは、@User.Identity?.Name さん!</a>
    </li>
    <li class="nav-item">
        <form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Action("Index", "Home", new { area = "" })" method="post">
            <button type="submit" class="nav-link btn btn-link text-dark">ログアウト</button>
        </form>
    </li>
}
else
{
    <li class="nav-item">
        <a class="nav-link text-dark" href="/Identity/Account/Register">新規登録</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" href="/Identity/Account/Login">ログイン</a>
    </li>
}
</ul>

_ViewImports.cshtmlが作成されていないため上手く動作しないことが判明しました
これまで同様にAreas/Identity/Pages フォルダを右クリックしてビューを追加
名前を「_ViewImports」としてコードを書きます

@using Microsoft.AspNetCore.Identity
@using TaskManager.Areas.Identity
@using TaskManager.Areas.Identity.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

続けて_ViewStart.cshtmlが以下のコードになっているか確認

@{
    Layout = "/Views/Shared/_Layout.cshtml";
}

完成

上記で修正が完了したので実行してみます…
結果は…無事に日本語化も成功しタスク作成が出来ることを確認出来ました!!!
実際に完成した画面を紹介します

新規ユーザー登録画面

ログイン画面
タスク作成画面
タスク一覧画面

最後に

一旦アプリを完成させて最低限動作させることまで出来ました
しかし、プロジェクト項目を作ったもののプロジェクト作成機能がないためアップデートしてさらに便利なツールとしてこれからも勉強がてら修正を加えていきたいと思います
修正内容は次回以降追って報告はしませんが納得の出来る形になればまた皆さんにさらに便利になったタスク管理アプリがご紹介できればと思います。
途中のスクショがあまりなくコードばかりになってしまって見づらい記事となってしまい申し訳ございませんが、こちらのコードを書くだけであなたのPCでもタスク管理アプリが作成出来るので是非勉強のつもりで実践してみて下さい
では、また次回の記事でお会いしましょう!!