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

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

目次

目次
はじめに
プロジェクト作成
DB接続準備
DB設計
PostgreSQL接続文字列の設定
Modelクラス追加
DbContext作成
マイグレーション実行
コントローラーの作成
ビューの作成
仕上げ
実行

はじめに

今回も引き続きプログラミング学習の一環としてC#でアプリ作成をしていきたいと思います。
今回作成するのは「タスク管理アプリ」です。
私自身、タスク管理が苦手でGoogleカレンダーに予定を入れたり、ToDoリストに書いたり
メモアプリを使用したり、紙に記載したりと色々試しましたがどれも続かずしっくりこないので折角だから
勉強がてら自分で作ってみようと思い今回作成を決意しました。

プロジェクト作成

それではこれから毎度のようにVisual studioを立ち上げてプロジェクトを作成するところから始めます。
Visual studioを立ち上げて「新しいプロジェクトの作成」をクリック
「ASP.NET Core Web アプリ (Model-View-Controller)」を選択
プロジェクト名は「TaskManager」、保存先は任意の場所を選択し次へ
追加情報部分は、今回のタスク管理アプリは少し難易度を上げてデータベース連携やユーザー認証を付けたいので、
認証の種類は「個別のアカウント」を選択して作成をクリック

DB接続準備

DBには「PostgreSQL」を使用するので事前準備としてNuGetパッケージをインストールしておきます
Visual Studioのパッケージマネージャーコンソールを開いて下記のコマンドを実行します

Install-Package Microsoft.AspNetCore.Identity.EntityFrameworkCore -Version 8.0.10
Install-Package Npgsql.EntityFrameworkCore.PostgreSQL -Version 8.0.10
Install-Package Microsoft.EntityFrameworkCore.Tools -Version 8.0.10

今回は.NET8.0を使用しているので各パッケージもバージョンを合わせるためにバージョン指定をしてインストールをしています
バージョンを合わせないと互換性がないとエラーが出てしまいます。

DB設計

テーブル構成は以下の通りとします

Tasks(タスクテーブル)
Id主キー
Titleタイトル
Description説明
DueDate期限
Priority優先度: 高/中/低
Statusステータス: 未着手/進行中/完了
CreatedAt作成日時
UpdatedAt更新日時
UserIdユーザーID – 外部キー
ProjectIdプロジェクトID – 外部キー、nullable
Projects(プロジェクトテーブル)
Id 主キー
Nameプロジェクト名
Description説明
UserIdユーザーID – 外部キー
CreatedAt作成日時
Tags(タグテーブル)
Id主キー
Nameタグ名
UserIdユーザーID – 外部キー
TaskTags(中間テーブル – 多対多の関係)
TaskId外部キー
TagId外部キー

PostgreSQL接続文字列の設定

appsettings.jsonに下記のコードを記載します
コードを書く際には以下のことに注意してください

Host=localhost → PostgreSQLが動いているサーバー(ローカルの場合はそのまま)
Database=TaskManagerDb → 作成するデータベース名
Username=postgres → PostgreSQLのユーザー名
Password=PostgreSQLにログインする際のパスワードを入力

{
  "ConnectionStrings": {
    "DefaultConnection": "Host=localhost;Database=TaskManagerDb;Username=postgres;Password=あなたのパスワード"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

Modelクラス追加

次にModelフォルダに新しいクラスを追加していきます
クラスの追加方法は追加したいフォルダを選択して右クリック→追加→クラスより追加できます
まずはTaskPriority.csという名前のクラスを追加し以下のコードを記載します

namespace TaskManager.Models
{
    public enum TaskPriority
    {
        Low = 0,      // 低
        Medium = 1,   // 中
        High = 2      // 高
    }
}

続けてTaskStatus.csという名前のクラスを追加し以下のコードを記載します

namespace TaskManager.Models
{
    public enum TaskStatus
    {
        NotStarted = 0,  // 未着手
        InProgress = 1,  // 進行中
        Completed = 2    // 完了
    }
}

続けてProject.csを追加します

using System.ComponentModel.DataAnnotations;

namespace TaskManager.Models
{
    public class Project
    {
        public int Id { get; set; }

        [Required(ErrorMessage = "プロジェクト名は必須です")]
        [StringLength(100)]
        public string Name { get; set; } = string.Empty;

        [StringLength(500)]
        public string? Description { get; set; }

        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

        [Required]
        public string UserId { get; set; } = string.Empty;

        public ICollection<TaskItem> Tasks { get; set; } = new List<TaskItem>();
    }
}

続けてTag.csを追加します

using System.ComponentModel.DataAnnotations;

namespace TaskManager.Models
{
    public class Tag
    {
        public int Id { get; set; }

        [Required(ErrorMessage = "タグ名は必須です")]
        [StringLength(50)]
        public string Name { get; set; } = string.Empty;

        [Required]
        public string UserId { get; set; } = string.Empty;

        public ICollection<TaskItem> Tasks { get; set; } = new List<TaskItem>();
    }
}

最後にメインのTaskItem.csを追加します。

using System.ComponentModel.DataAnnotations;

namespace TaskManager.Models
{
    public class TaskItem
    {
        public int Id { get; set; }

        [Required(ErrorMessage = "タイトルは必須です")]
        [StringLength(200, ErrorMessage = "タイトルは200文字以内で入力してください")]
        public string Title { get; set; } = string.Empty;

        [StringLength(1000, ErrorMessage = "説明は1000文字以内で入力してください")]
        public string? Description { get; set; }

        [DataType(DataType.Date)]
        public DateTime? DueDate { get; set; }

        public TaskPriority Priority { get; set; } = TaskPriority.Medium;

        public TaskStatus Status { get; set; } = TaskStatus.NotStarted;

        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

        public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;

        // ユーザーとのリレーション
        [Required]
        public string UserId { get; set; } = string.Empty;

        // プロジェクトとのリレーション
        public int? ProjectId { get; set; }
        public Project? Project { get; set; }

        // タグとのリレーション
        public ICollection<Tag> Tags { get; set; } = new List<Tag>();
    }
}

DbContext作成

DataフォルダにApplicationDbContext.csを追加します
※既にある場合はそちらの中身を以下のコードに書き換えます

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

namespace TaskManager.Data
{
    public class ApplicationDbContext : IdentityDbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        // DbSetを追加、完全修飾名
        public DbSet<TaskItem> TaskItems { get; set; } = null!;
        public DbSet<Project> Projects { get; set; } = null!;
        public DbSet<Tag> Tags { get; set; } = null!;

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // TaskItems
            modelBuilder.Entity<TaskItem>().ToTable("TaskItems");

            // TaskとTagの多対多リレーションシップ設定
            modelBuilder.Entity<TaskItem>()
                .HasMany(t => t.Tags)
                .WithMany(tag => tag.Tasks)
                .UsingEntity(j => j.ToTable("TaskTags"));

            // TaskとProjectのリレーションシップ設定
            modelBuilder.Entity<TaskItem>()
                .HasOne(t => t.Project)
                .WithMany(p => p.Tasks)
                .HasForeignKey(t => t.ProjectId)
                .OnDelete(DeleteBehavior.SetNull);

            // インデックス追加
            modelBuilder.Entity<TaskItem>()
                .HasIndex(t => t.Status);

            modelBuilder.Entity<TaskItem>()
                .HasIndex(t => t.DueDate);

            modelBuilder.Entity<TaskItem>()
                .HasIndex(t => t.UserId);
        }
    }
}

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();

マイグレーション実行

ここまでコードが書けたらマイグレーションを実行します
パッケージマネージャーコンソールに下記のコマンドを入力します
実行に成功するとPostgreSQLにテーブルが自動で生成されます

Add-Migration InitialCreate
Update-Database

成功したのでPostgreSQLを開いてテーブルが出来ているか確認します
無事にテーブルが作成されています!!

コントローラーの作成

DBの構築が成功したので続けてコントローラーを作成していきます。
Controllersフォルダで右クリック → 追加 → コントローラー
「MVCコントローラー – 空」を選択し、名前を 「TaskItemsController」にして作成
以下のコードを記載

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TaskManager.Data;
using TaskManager.Models;
using System.Security.Claims;

namespace TaskManager.Controllers
{
    [Authorize]
    public class TaskItemsController : Controller
    {
        private readonly ApplicationDbContext _context;

        public TaskItemsController(ApplicationDbContext context)
        {
            _context = context;
        }

        // 一覧表示(GET: TaskItems)
        public async Task<IActionResult> Index()
        {
            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
            
            var tasks = await _context.TaskItems
                .Include(t => t.Project)
                .Include(t => t.Tags)
                .Where(t => t.UserId == userId)
                .OrderByDescending(t => t.CreatedAt)
                .ToListAsync();

            return View(tasks);
        }

        // 詳細表示(GET: TaskItems/Details/5)
        public async Task<IActionResult> Details(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
            
            var taskItem = await _context.TaskItems
                .Include(t => t.Project)
                .Include(t => t.Tags)
                .FirstOrDefaultAsync(m => m.Id == id && m.UserId == userId);

            if (taskItem == null)
            {
                return NotFound();
            }

            return View(taskItem);
        }

        // 作成画面表示(GET: TaskItems/Create)
        public IActionResult Create()
        {
            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
            
            ViewBag.Projects = _context.Projects
                .Where(p => p.UserId == userId)
                .ToList();

            return View();
        }

        // 作成処理(POST: TaskItems/Create)
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("Title,Description,DueDate,Priority,Status,ProjectId")] TaskItem taskItem)
        {
            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
            
            // バリデーションエラーをスキップして強制的に保存
            ModelState.Clear();
            
            taskItem.UserId = userId;
            taskItem.CreatedAt = DateTime.UtcNow;
            taskItem.UpdatedAt = DateTime.UtcNow;
            
            _context.Add(taskItem);
            await _context.SaveChangesAsync();
            
            return RedirectToAction(nameof(Index));
        }

        // 編集画面表示(GET: TaskItems/Edit/5)
        public async Task<IActionResult> Edit(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
            
            var taskItem = await _context.TaskItems
                .FirstOrDefaultAsync(t => t.Id == id && t.UserId == userId);

            if (taskItem == null)
            {
                return NotFound();
            }

            ViewBag.Projects = _context.Projects
                .Where(p => p.UserId == userId)
                .ToList();

            return View(taskItem);
        }

        // 編集処理(POST: TaskItems/Edit/5)
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(int id, [Bind("Id,Title,Description,DueDate,Priority,Status,ProjectId,CreatedAt")] TaskItem taskItem)
        {
            if (id != taskItem.Id)
            {
                return NotFound();
            }

            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);

            // バリデーションエラーをスキップ
            ModelState.Clear();
            
            try
            {
                taskItem.UserId = userId;
                taskItem.UpdatedAt = DateTime.UtcNow;
                
                _context.Update(taskItem);
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!TaskItemExists(taskItem.Id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }
            return RedirectToAction(nameof(Index));
        }

        // 削除確認画面(GET: TaskItems/Delete/5)
        public async Task<IActionResult> Delete(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
            
            var taskItem = await _context.TaskItems
                .Include(t => t.Project)
                .FirstOrDefaultAsync(m => m.Id == id && m.UserId == userId);

            if (taskItem == null)
            {
                return NotFound();
            }

            return View(taskItem);
        }

        // 削除処理(POST: TaskItems/Delete/5)
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(int id)
        {
            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
            
            var taskItem = await _context.TaskItems
                .FirstOrDefaultAsync(t => t.Id == id && t.UserId == userId);

            if (taskItem != null)
            {
                _context.TaskItems.Remove(taskItem);
                await _context.SaveChangesAsync();
            }

            return RedirectToAction(nameof(Index));
        }

        private bool TaskItemExists(int id)
        {
            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
            return _context.TaskItems.Any(e => e.Id == id && e.UserId == userId);
        }
    }
}

ビューの作成

Views/TaskItems フォルダを作成
Viewsフォルダで右クリック→追加 → 新しいフォルダー
フォルダ名を「TaskItems」にして作成

1つ目: Index.cshtml(一覧画面)を作成
Views/TaskItems フォルダで右クリック → 追加 → ビュー → Razorビュー
名前はを「Index」 にして作成
以下のコードを記載

@model IEnumerable<TaskManager.Models.TaskItem>

@{
    ViewData["Title"] = "タスク一覧";
}

<div class="container mt-4">
    <div class="d-flex justify-content-between align-items-center mb-4">
        <h1>@ViewData["Title"]</h1>
        <a asp-action="Create" class="btn btn-primary">
            <i class="bi bi-plus-circle"></i> 新しいタスク
        </a>
    </div>

    @if (!Model.Any())
    {
        <div class="alert alert-info">
            <i class="bi bi-info-circle"></i> タスクがまだありません。新しいタスクを作成しましょう!
        </div>
    }
    else
    {
        <div class="row">
            @foreach (var item in Model)
            {
                <div class="col-md-6 col-lg-4 mb-3">
                    <div class="card h-100 shadow-sm">
                        <div class="card-body">
                            <h5 class="card-title">
                                @Html.DisplayFor(modelItem => item.Title)
                            </h5>

                            @if (!string.IsNullOrEmpty(item.Description))
                            {
                                <p class="card-text text-muted">
                                    @(item.Description.Length > 100 ? item.Description.Substring(0, 100) + "..." : item.Description)
                                </p>
                            }

                            <div class="mb-2">
                                @switch (item.Priority)
                                {
                                    case TaskManager.Models.TaskPriority.High:
                                        <span class="badge bg-danger">高優先度</span>
                                        break;
                                    case TaskManager.Models.TaskPriority.Medium:
                                        <span class="badge bg-warning text-dark">中優先度</span>
                                        break;
                                    case TaskManager.Models.TaskPriority.Low:
                                        <span class="badge bg-secondary">低優先度</span>
                                        break;
                                }

                                @switch (item.Status)
                                {
                                    case TaskManager.Models.TaskStatus.NotStarted:
                                        <span class="badge bg-light text-dark">未着手</span>
                                        break;
                                    case TaskManager.Models.TaskStatus.InProgress:
                                        <span class="badge bg-info">進行中</span>
                                        break;
                                    case TaskManager.Models.TaskStatus.Completed:
                                        <span class="badge bg-success">完了</span>
                                        break;
                                }
                            </div>

                            @if (item.DueDate.HasValue)
                            {
                                <p class="card-text">
                                    <small class="text-muted">
                                        <i class="bi bi-calendar"></i>
                                        期限: @item.DueDate.Value.ToLocalTime().ToString("yyyy/MM/dd")
                                    </small>
                                </p>
                            }

                            @if (item.Project != null)
                            {
                                <p class="card-text">
                                    <small class="text-muted">
                                        <i class="bi bi-folder"></i> @item.Project.Name
                                    </small>
                                </p>
                            }
                        </div>
                        <div class="card-footer bg-transparent">
                            <a asp-action="Details" asp-route-id="@item.Id" class="btn btn-sm btn-outline-primary">詳細</a>
                            <a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-sm btn-outline-secondary">編集</a>
                            <a asp-action="Delete" asp-route-id="@item.Id" class="btn btn-sm btn-outline-danger">削除</a>
                        </div>
                    </div>
                </div>
            }
        </div>
    }
</div>

2つ目: Create.cshtml を作成
Views/TaskItems フォルダを右クリック → 追加 → ビュー → Razorビュー
名前を「Create」 にして作成
以下のコードを記載

@model TaskManager.Models.TaskItem
@{
    ViewData["Title"] = "新しいタスクを作成";
}

<div class="container mt-4">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <h1>@ViewData["Title"]</h1>
            <hr />
            
            <form action="/TaskItems/Create" method="post">
                @Html.AntiForgeryToken()
                
                <div class="mb-3">
                    <label for="Title" class="form-label">タイトル <span class="text-danger">*</span></label>
                    <input type="text" id="Title" name="Title" class="form-control" placeholder="タスクのタイトルを入力" required />
                </div>
                
                <div class="mb-3">
                    <label for="Description" class="form-label">説明</label>
                    <textarea id="Description" name="Description" class="form-control" rows="4" placeholder="タスクの詳細を入力"></textarea>
                </div>
                
                <div class="row">
                    <div class="col-md-6 mb-3">
                        <label for="Priority" class="form-label">優先度</label>
                        <select id="Priority" name="Priority" class="form-select">
                            <option value="0">低</option>
                            <option value="1" selected>中</option>
                            <option value="2">高</option>
                        </select>
                    </div>
                    
                    <div class="col-md-6 mb-3">
                        <label for="Status" class="form-label">ステータス</label>
                        <select id="Status" name="Status" class="form-select">
                            <option value="0" selected>未着手</option>
                            <option value="1">進行中</option>
                            <option value="2">完了</option>
                        </select>
                    </div>
                </div>
                
                <div class="row">
                    <div class="col-md-6 mb-3">
                        <label for="DueDate" class="form-label">期限</label>
                        <input type="date" id="DueDate" name="DueDate" class="form-control" />
                    </div>
                    
                    <div class="col-md-6 mb-3">
                        <label for="ProjectId" class="form-label">プロジェクト</label>
                        <select id="ProjectId" name="ProjectId" class="form-select">
                            <option value="">プロジェクトなし</option>
                            @if (ViewBag.Projects != null)
                            {
                                @foreach (var project in ViewBag.Projects as List<TaskManager.Models.Project>)
                                {
                                    <option value="@project.Id">@project.Name</option>
                                }
                            }
                        </select>
                    </div>
                </div>
                
                <div class="mb-3">
                    <button type="submit" class="btn btn-primary">
                        作成
                    </button>
                    <a href="/TaskItems/Index" class="btn btn-secondary">
                        キャンセル
                    </a>
                </div>
            </form>
        </div>
    </div>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

3つ目: Details.cshtml(詳細画面)を作成
Views/TaskItems フォルダを右クリック → 追加 → ビュー → Razorビュー
名前を「Details」 にして作成
以下のコードを記載

@model TaskManager.Models.TaskItem

@{
    ViewData["Title"] = "タスク詳細";
}

<div class="container mt-4">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="d-flex justify-content-between align-items-center mb-3">
                <h1>@ViewData["Title"]</h1>
                <div>
                    <a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-warning">
                        <i class="bi bi-pencil"></i> 編集
                    </a>
                    <a asp-action="Index" class="btn btn-secondary">
                        <i class="bi bi-arrow-left"></i> 一覧に戻る
                    </a>
                </div>
            </div>

            <div class="card shadow-sm">
                <div class="card-body">
                    <h2 class="card-title mb-4">@Html.DisplayFor(model => model.Title)</h2>

                    <dl class="row">
                        <dt class="col-sm-3">説明</dt>
                        <dd class="col-sm-9">
                            @if (!string.IsNullOrEmpty(Model.Description))
                            {
                                <p style="white-space: pre-wrap;">@Model.Description</p>
                            }
                            else
                            {
                                <span class="text-muted">説明なし</span>
                            }
                        </dd>

                        <dt class="col-sm-3">優先度</dt>
                        <dd class="col-sm-9">
                            @switch (Model.Priority)
                            {
                                case TaskManager.Models.TaskPriority.High:
                                    <span class="badge bg-danger">高優先度</span>
                                    break;
                                case TaskManager.Models.TaskPriority.Medium:
                                    <span class="badge bg-warning text-dark">中優先度</span>
                                    break;
                                case TaskManager.Models.TaskPriority.Low:
                                    <span class="badge bg-secondary">低優先度</span>
                                    break;
                            }
                        </dd>

                        <dt class="col-sm-3">ステータス</dt>
                        <dd class="col-sm-9">
                            @switch (Model.Status)
                            {
                                case TaskManager.Models.TaskStatus.NotStarted:
                                    <span class="badge bg-light text-dark">未着手</span>
                                    break;
                                case TaskManager.Models.TaskStatus.InProgress:
                                    <span class="badge bg-info">進行中</span>
                                    break;
                                case TaskManager.Models.TaskStatus.Completed:
                                    <span class="badge bg-success">完了</span>
                                    break;
                            }
                        </dd>

                        <dt class="col-sm-3">期限</dt>
                        <dd class="col-sm-9">
                            @if (Model.DueDate.HasValue)
                            {
                                <i class="bi bi-calendar"></i> 
                                @Model.DueDate.Value.ToLocalTime().ToString("yyyy年MM月dd日")
                            }
                            else
                            {
                                <span class="text-muted">期限なし</span>
                            }
                        </dd>

                        <dt class="col-sm-3">プロジェクト</dt>
                        <dd class="col-sm-9">
                            @if (Model.Project != null)
                            {
                                <i class="bi bi-folder"></i> 
                                @Model.Project.Name
                            }
                            else
                            {
                                <span class="text-muted">プロジェクトなし</span>
                            }
                        </dd>

                        <dt class="col-sm-3">作成日時</dt>
                        <dd class="col-sm-9">
                            @Model.CreatedAt.ToLocalTime().ToString("yyyy年MM月dd日 HH:mm")
                        </dd>

                        <dt class="col-sm-3">更新日時</dt>
                        <dd class="col-sm-9">
                            @Model.UpdatedAt.ToLocalTime().ToString("yyyy年MM月dd日 HH:mm")
                        </dd>
                    </dl>
                </div>
            </div>

            <div class="mt-3">
                <a asp-action="Delete" asp-route-id="@Model.Id" class="btn btn-danger">
                    <i class="bi bi-trash"></i> 削除
                </a>
            </div>
        </div>
    </div>
</div>

4つ目: Edit.cshtml(編集画面)を作成
Views/TaskItems フォルダを右クリック → 追加 → ビュー → Razorビュー
名前を「Edit」 にして作成
以下のコードを記載

@model TaskManager.Models.TaskItem

@{
    ViewData["Title"] = "タスクを編集";
}

<div class="container mt-4">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <h1>@ViewData["Title"]</h1>
            <hr />

            <form asp-action="Edit" method="post">
                <div asp-validation-summary="ModelOnly" class="text-danger"></div>

                <input type="hidden" asp-for="Id" />
                <input type="hidden" asp-for="CreatedAt" />

                <div class="mb-3">
                    <label asp-for="Title" class="form-label">タイトル <span class="text-danger">*</span></label>
                    <input asp-for="Title" class="form-control" placeholder="タスクのタイトルを入力" />
                    <span asp-validation-for="Title" class="text-danger"></span>
                </div>

                <div class="mb-3">
                    <label asp-for="Description" class="form-label">説明</label>
                    <textarea asp-for="Description" class="form-control" rows="4" placeholder="タスクの詳細を入力"></textarea>
                    <span asp-validation-for="Description" class="text-danger"></span>
                </div>

                <div class="row">
                    <div class="col-md-6 mb-3">
                        <label asp-for="Priority" class="form-label">優先度</label>
                        <select asp-for="Priority" class="form-select">
                            <option value="0">低</option>
                            <option value="1">中</option>
                            <option value="2">高</option>
                        </select>
                        <span asp-validation-for="Priority" class="text-danger"></span>
                    </div>

                    <div class="col-md-6 mb-3">
                        <label asp-for="Status" class="form-label">ステータス</label>
                        <select asp-for="Status" class="form-select">
                            <option value="0">未着手</option>
                            <option value="1">進行中</option>
                            <option value="2">完了</option>
                        </select>
                        <span asp-validation-for="Status" class="text-danger"></span>
                    </div>
                </div>

                <div class="row">
                    <div class="col-md-6 mb-3">
                        <label asp-for="DueDate" class="form-label">期限</label>
                        <input asp-for="DueDate" type="date" class="form-control" />
                        <span asp-validation-for="DueDate" class="text-danger"></span>
                    </div>

                    <div class="col-md-6 mb-3">
                        <label asp-for="ProjectId" class="form-label">プロジェクト</label>
                        <select asp-for="ProjectId" class="form-select">
                            <option value="">プロジェクトなし</option>
                            @foreach (var project in ViewBag.Projects as List<TaskManager.Models.Project>)
                            {
                                <option value="@project.Id">@project.Name</option>
                            }
                        </select>
                        <span asp-validation-for="ProjectId" class="text-danger"></span>
                    </div>
                </div>

                <div class="mb-3">
                    <button type="submit" class="btn btn-primary">
                        <i class="bi bi-check-circle"></i> 保存
                    </button>
                    <a asp-action="Details" asp-route-id="@Model.Id" class="btn btn-secondary">
                        <i class="bi bi-x-circle"></i> キャンセル
                    </a>
                </div>
            </form>
        </div>
    </div>
</div>

@section Scripts {
    @{
        await Html.RenderPartialAsync("_ValidationScriptsPartial");
    }
}

5つ目(最後!): Delete.cshtml(削除確認画面)を作成
Views/TaskItems フォルダを右クリック → 追加 → ビュー → Razorビュー
名前を「Delete」 にして作成
以下のコードを記載

@model TaskManager.Models.TaskItem

@{
    ViewData["Title"] = "タスクを削除";
}

<div class="container mt-4">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <h1 class="text-danger">@ViewData["Title"]</h1>
            <hr />

            <div class="alert alert-warning">
                <i class="bi bi-exclamation-triangle"></i>
                <strong>警告:</strong> このタスクを削除してもよろしいですか?この操作は取り消せません。
            </div>

            <div class="card shadow-sm">
                <div class="card-body">
                    <h3 class="card-title mb-4">@Html.DisplayFor(model => model.Title)</h3>

                    <dl class="row">
                        <dt class="col-sm-3">説明</dt>
                        <dd class="col-sm-9">
                            @if (!string.IsNullOrEmpty(Model.Description))
                            {
                                <p style="white-space: pre-wrap;">@Model.Description</p>
                            }
                            else
                            {
                                <span class="text-muted">説明なし</span>
                            }
                        </dd>

                        <dt class="col-sm-3">優先度</dt>
                        <dd class="col-sm-9">
                            @switch (Model.Priority)
                            {
                                case TaskManager.Models.TaskPriority.High:
                                    <span class="badge bg-danger">高優先度</span>
                                    break;
                                case TaskManager.Models.TaskPriority.Medium:
                                    <span class="badge bg-warning text-dark">中優先度</span>
                                    break;
                                case TaskManager.Models.TaskPriority.Low:
                                    <span class="badge bg-secondary">低優先度</span>
                                    break;
                            }
                        </dd>

                        <dt class="col-sm-3">ステータス</dt>
                        <dd class="col-sm-9">
                            @switch (Model.Status)
                            {
                                case TaskManager.Models.TaskStatus.NotStarted:
                                    <span class="badge bg-light text-dark">未着手</span>
                                    break;
                                case TaskManager.Models.TaskStatus.InProgress:
                                    <span class="badge bg-info">進行中</span>
                                    break;
                                case TaskManager.Models.TaskStatus.Completed:
                                    <span class="badge bg-success">完了</span>
                                    break;
                            }
                        </dd>

                        <dt class="col-sm-3">期限</dt>
                        <dd class="col-sm-9">
                            @if (Model.DueDate.HasValue)
                            {
                                <i class="bi bi-calendar"></i> 
                                @Model.DueDate.Value.ToLocalTime().ToString("yyyy年MM月dd日")
                            }
                            else
                            {
                                <span class="text-muted">期限なし</span>
                            }
                        </dd>

                        <dt class="col-sm-3">プロジェクト</dt>
                        <dd class="col-sm-9">
                            @if (Model.Project != null)
                            {
                                <i class="bi bi-folder"></i> 
                                @Model.Project.Name
                            }
                            else
                            {
                                <span class="text-muted">プロジェクトなし</span>
                            }
                        </dd>

                        <dt class="col-sm-3">作成日時</dt>
                        <dd class="col-sm-9">
                            @Model.CreatedAt.ToLocalTime().ToString("yyyy年MM月dd日 HH:mm")
                        </dd>
                    </dl>
                </div>
            </div>

            <form asp-action="Delete" method="post" class="mt-3">
                <input type="hidden" asp-for="Id" />
                <button type="submit" class="btn btn-danger">
                    <i class="bi bi-trash"></i> 削除する
                </button>
                <a asp-action="Index" class="btn btn-secondary">
                    <i class="bi bi-x-circle"></i> キャンセル
                </a>
            </form>
        </div>
    </div>
</div>

仕上げ

最後にナビゲーションメニューを追加します
Views/Shared/_Layout.cshtmlのナビゲーションバーの部分にタスク管理へのリンクを追加します
_Layout.cshtml の全コードは以下の通りです

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - TaskManager</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/TaskManager.styles.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container-fluid">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">TaskManager</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <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>
                    <partial name="_LoginPartial" />
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2025 - TaskManager - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

実行

これで一通りプログラムが完成したので実行してみます
結果は…画面自体は表示されましたが英語表記でぱっと見でわかりづらかったり
一部ボタンが反応しなかったりしたので修正を加えていきます
今回はここまでにして修正編は次回執筆していきますのでお楽しみに!!