前回に引き続き、具体的なTodoリストアプリ開発を題材として、Blazorの基本機能について紹介します。
初心者がBlazorでWebアプリ開発をする際にまず押さえるべき以下の要点を学びます。
- C#コードとHTMLを組み合わせて動的なページを作るRazorコンポーネント
- データベース(DB)アクセスを行うためのEntity Framework Core
- 認証基盤であるASP.NET Core Identity
「最終ゴール」は、認証機能付きでDBへの参照・更新操作を行う簡易なTodoリストアプリを作ることです。
前回は以下の部分を作りました。
- Entity Framework Core(EF Core)でクラス定義からDBスキーマを生成
- EF Coreを使ってTodo一覧を取得するビジネスロジックを実装
- RazorコンポーネントでTodo一覧を表示
前回の記事は以下になりますので、ぜひまずは先にこちらをご覧ください。
今回は以下について学びます。
- EF Coreを使ってTodoを更新・削除・追加するビジネスロジックを実装
- Razorコンポーネントを部品化して再利用
これによって、DBのCRUD操作を一通りできるようにします。加えて、Razorコンポーネントの部品化と再利用について学び、保守性の高いコードを書けるようにします。
完成イメージは次のようになります。
演習コード一式をGitHubで公開しているので参考にしてください。
簡単なアプリではありますが、これでDBの参照・更新ができるようになり、Webアプリの基本であるCRUD操作が完成します!
YouTubeも解説動画も作っていますので、あわせてご覧ください。
演習1:Todoを更新・追加・削除 ~DBへのCRUD操作~
作る機能
Todoリストアプリを以下のように段階的に作り上げていきます。
- 【1】Todoリストを一覧で表示できる。(←前回記事の演習)
- 【2】Todoを削除、編集、追加できる。(←今回記事の演習)
- 【3】ユーザごとにTodoリストを管理できる。
- 【4】画面入力に対してバリデーションを行える。
- 【5】DB更新の結果(成功・失敗)を画面UI上に表示できる。
前回のコードをベースとして、Todoを更新・追加・削除する機能を追加します。
(Readme.mdに書いていますが、このコードにはDBは含まれていないため、最初に「dotnet ef database update」コマンドで作ってください。)
「画面」と「ビジネスロジック」のコードを修正します。
データモデル・DBはTodoItemクラス(TodoItemテーブル)をそのまま使うので手を加える必要はありません。
具体的には次のようにコードを修正します。
- 手順1:TodoListService.cs (ビジネスロジック)
- 手順2:TodoList.razor (画面)
手順1:ビジネスロジックを作成
ポイント
EF Coreを使うとSQLクエリを直接書かずにDB操作を行うことができます。
DBの各テーブルに対応するC#のコレクションを操作すると、EF Coreが裏でDB(今回はSQLite)に対応するSQLクエリを発行してくれるわけですね。
EF Coreでは遅延実行という仕組みがあり、以下の2ステップでDBアクセスを行います。
- Step1:クエリを定義 (この時点ではDBアクセスはしない)
- Step2:クエリを実行して結果を取得 (ここでDBアクセスをする)
参照系(要素の参照)、更新系(要素の更新・追加・削除)のイメージを示します。
要するに、結果が本当に必要なタイミングまで実際のDBアクセスを遅延させているのですね。
これにより、DBクエリの発行をなるべく少なく抑え、効率よくDBアクセスを行えます。
コード
遅延実行の考え方を踏まえて、TodoListService.csのコードを以下のように書き換えましょう。
using Microsoft.EntityFrameworkCore;
using TodoListApp.Data;
using TodoListApp.Models;
namespace TodoListApp.Services;
public class TodoListService
{
private readonly ApplicationDbContext _context;
public TodoListService(ApplicationDbContext context)
{
_context = context;
}
public async Task<List<TodoItem>> GetTodosAsync()
{
var todoItems = await _context.TodoItems.ToListAsync();
return todoItems;
}
public async Task UpdateTodoAsync(TodoItem todoItemInput)
{
var existingTodoItem = await _context.TodoItems.FindAsync(todoItemInput.Id);
if (existingTodoItem != null)
{
existingTodoItem.Title = todoItemInput.Title;
await _context.SaveChangesAsync();
}
}
public async Task DeleteTodoAsync(int id)
{
var existingTodoItem = await _context.TodoItems.FindAsync(id);
if (existingTodoItem != null)
{
_context.TodoItems.Remove(existingTodoItem);
await _context.SaveChangesAsync();
}
}
public async Task AddTodoAsync(TodoItem newTodoItem)
{
_context.TodoItems.Add(newTodoItem);
await _context.SaveChangesAsync();
}
}
参照系・更新系のどちらもTodoItemクラスに対応するDBテーブルへアクセスするための「_context.TodoItems」を通してDB操作を行います。
参照系のGetTodosAsyncメソッドでは次のようになります。
- Step1:「_context.TodoItems」まででクエリを構築
- Step2:「_context.TodoItems.ToListAsync()」でクエリ実行と結果取得
更新系の例えばUpdateTodoAsyncメソッドでは次のようになります。
…省略…
//ここからクエリ構築開始↓
var existingTodoItem = await _context.TodoItems.FindAsync(todoItemInput.Id);
if (existingTodoItem != null)
{
existingTodoItem.Title = todoItemInput.Title;
//ここまでがクエリ構築↑
await _context.SaveChangesAsync(); //ここでクエリ実行と結果取得
…省略…
ちなみに、プログラムをデバッグ実行すると発行されたSQLクエリがデバッグ出力に表示されます。
クエリがどのタイミングでどのような内容で発行されているか?に興味がある人は確認してみるとよいです。
例えば、上述したUpdateTodoAsyncを実行したときの出力例は以下のようになります。UPDATEクエリが発行されていますね。
手順2:画面を作成
画面設計
今回はなるべくシンプルにするため、一覧表示した各Todo要素に更新・削除用のボタンを配置し、下側に新Todo追加のボタンを設置します。
更新は「Edit」ボタンが押されたら編集用のテキストボックスを表示させて、「Save」ボタンが押されたら変更を反映するようにします。
以下のようなイメージです。
ポイント
Razorコンポーネントは「見た目」と「振る舞い」で画面のコードを記述します。
「見た目」部分はUI構造を定義するRazor構文で記述し、「振る舞い」部分は状態変数やイベント実行コードをC#コードで記述します。
以下に、Razorコンポーネントの基本的な考え方を示します。
状態変数とバインディング
状態変数はページ構造に埋め込むことができます。状態変数が変わると、UI構造も自動的に更新されます。
さらに、ページの入力フォームなどを使うと、ユーザが入力を行うたびに状態変数の値が自動で更新されます。
このような状態変数とページ構造を自動同期させる仕組みをバインディング(データバインディング)と呼びます。
バインディングはBlazorで動的なページを作るときの重要な考え方です。
React・Vue.jsなど、他のモダンなWebフロントエンドのフレームワークでも取り入れられている考え方です。
イベントの実行
ページ構造に関連付けられたイベントが呼び出されると、対応するメソッドが実行されます。
例えば、ボタンがクリックされると、そのクリックイベントに関連付けられたC#コードが実行されるといった感じです。
コード
TodoList.razorのコードを見てみましょう。
@page "/todolist"
@rendermode InteractiveServer
@using TodoListApp.Models
@using TodoListApp.Services
@inject TodoListService _todoService
<PageTitle>TodoList</PageTitle>
<h1>Todo List</h1>
<ul>
@foreach (var todoItem in _todoItems)
{
<li>
<span>Id: @todoItem.Id, @todoItem.Title</span>
@if (_editingTodoItem != null && _editingTodoItem.Id == todoItem.Id)
{
<input @bind="_editingTodoItem.Title" placeholder="Edit title" />
<button @onclick="UpdateTodoAsync">Save</button>
}
else
{
<button @onclick="() => EnterEditMode(todoItem)">Edit</button>
}
<button @onclick="() => DeleteTodoAsync(todoItem.Id)">Delete</button>
</li>
}
</ul>
<input @bind="_newTodoItem.Title" placeholder="New todo title" />
<button @onclick="AddTodoAsync">Add</button>
@code {
private List<TodoItem> _todoItems = new();
private TodoItem _newTodoItem = new TodoItem();
private TodoItem? _editingTodoItem;
protected override async Task OnInitializedAsync()
{
await ReloadTodosAsync();
}
private async Task ReloadTodosAsync()
{
_todoItems = await _todoService.GetTodosAsync();
}
private void EnterEditMode(TodoItem todoItem)
{
_editingTodoItem = new TodoItem
{
Id = todoItem.Id,
Title = todoItem.Title
};
}
private async Task UpdateTodoAsync()
{
if (_editingTodoItem != null)
{
await _todoService.UpdateTodoAsync(_editingTodoItem);
_editingTodoItem = null;
await ReloadTodosAsync();
}
}
private async Task DeleteTodoAsync(int id)
{
await _todoService.DeleteTodoAsync(id);
await ReloadTodosAsync();
}
private async Task AddTodoAsync()
{
await _todoService.AddTodoAsync(_newTodoItem);
_newTodoItem = new TodoItem();
await ReloadTodosAsync();
}
}
前節で説明した考え方に基づいて具体的にどのように記述されているか、いくつか例をあげて簡単に解説します。
次のように@code{…}の先頭で状態変数を定義しています。
@code {
private List<TodoItem> _todoItems = new();
private TodoItem _newTodoItem = new TodoItem();
private TodoItem? _editingTodoItem;
以下ではRazor構文を使い、状態変数を埋め込みながらページ構造を記述しています。また、クリック時のイベント(メソッド)呼び出しの設定もしています。
@foreach (var todoItem in _todoItems)
{
<li>
<span>Id: @todoItem.Id, @todoItem.Title</span>
@if (_editingTodoItem != null && _editingTodoItem.Id == todoItem.Id)
{
<input @bind="_editingTodoItem.Title" placeholder="Edit title" />
<button @onclick="UpdateTodoAsync">Save</button>
}
else
{
<button @onclick="() => EnterEditMode(todoItem)">Edit</button>
}
<button @onclick="() => DeleteTodoAsync(todoItem.Id)">Delete</button>
</li>
}
表示するTodoの要素ごとに、その要素を編集したり削除したりするためのイベントを設定しています。
@ifの部分で、編集対象のTodo要素がある場合は編集用のフォームを表示するようにしています。
編集用フォームを表示する以下の部分を見てください。
<input @bind="_editingTodoItem.Title" placeholder="Edit title" />
<button @onclick="UpdateTodoAsync">Save</button>
ここで、編集用フォームへユーザが入力した値を状態変数_editingTodoItem.Titleへ反映させるようにバインドしています。
@code{…}では、UpdateTodoAsync、DeleteTodoAsyncなどのCRUD操作に対応するイベントのコードを記述しています。
それぞれがビジネスロジックTodoListServiceクラスのCRUD操作に対応するメソッドを呼び出し実際のDBアクセスを行っています。
アプリ実行
Visual Studioでデバッグ実行してみてください。
Todoの追加・削除・編集の操作をいろいろ試してみましょう。
これでDBのCRUD操作が一通りできたよ!
演習2:Razorコンポーネントの部品化と再利用 ~保守性の高いコード~
演習1のコードを少し整理して、Razorコンポーネントの部品化と再利用について学びましょう。
部品化・再利用によってコードの保守性が高くなり、あとからコードの内容を思い出したり修正して機能を追加したりといったことが行いやすくなります。
以下のようなイメージです。
オブジェクト指向プログラミングにおける「部品化/再利用」の基本についてはぜひ以下を参考にしてください。
演習1コードにおける問題点
演習1でページ構造を表すRazor構文における「Todo編集」と「Todo追加」の部分を見てください。
<ul>
@foreach (var todoItem in _todoItems)
…省略…
<input @bind="_editingTodoItem.Title" placeholder="Edit title" />
<button @onclick="UpdateTodoAsync">Save</button>
…省略…
</ul>
<input @bind="_newTodoItem.Title" placeholder="New todo title" />
<button @onclick="AddTodoAsync">Add</button>
このコードはどちらも「TodoのTitleをユーザが編集し、ボタンを押したらイベント(更新か新規追加)を実行する」という処理を行っているため、よく似ていますね。
演習2ではRazorコンポーネントの部品を作り、この部分を共通化してみましょう。
演習2のコード
以下のように、Shared/TodoItemEditor.razorというTodo編集用のコンポーネントを作成し、TodoList.razorでそれを使うよう修正します。
Todo編集用のコンポーネントを定義
TodoItemEditor.razorのコードは以下のようになります。
@using TodoListApp.Models
<input @bind="TodoItem.Title" placeholder="@Placeholder" />
<button @onclick="OnSubmit">@ButtonText</button>
@code {
[Parameter] public TodoItem TodoItem { get; set; } = new TodoItem();
[Parameter] public string ButtonText { get; set; } = "Submit";
[Parameter] public string Placeholder { get; set; } = "";
[Parameter] public Func<Task> OnSubmit { get; set; } = () => Task.CompletedTask;
}
[Parameter]属性を付与されてるプロパティが、このRazorコンポーネントのパラメータとなります。
Todo編集用のコンポーネントを利用
「@using TodoListApp.Components.Shared」を追記します。
それぞれの更新・追加における編集フォーム部分(<input… </button>の部分)をTodoItemEditorで置き換えます。
…省略
@*★↓追加*@
@using TodoListApp.Components.Shared
…省略…
<ul>
@foreach (var todoItem in _todoItems)
{
<li>
<span>Id: @todoItem.Id, @todoItem.Title</span>
@if (_editingTodoItem != null && _editingTodoItem.Id == todoItem.Id)
{
@*★↓ <input … </button> の部分をTodoItemEditorで置き換え*@
<TodoItemEditor TodoItem="_editingTodoItem" OnSubmit="UpdateTodoAsync" Placeholder="Edit title" ButtonText="Save" />
}
else
{
<button @onclick="() => EnterEditMode(todoItem)">Edit</button>
}
<button @onclick="() => DeleteTodoAsync(todoItem.Id)">Delete</button>
</li>
}
</ul>
@*★↓ <input … </button> の部分をTodoItemEditorで置き換え*@
<TodoItemEditor TodoItem="_newTodoItem" OnSubmit="AddTodoAsync" Placeholder="New todo title" ButtonText="Add" />
Razorコンポーネントを使って、コードを共通化することができました。
これで少し保守性が向上しましたね。
アプリ実行時の動作は、演習1のものと同じです。
この編集用フォームには、入力チェックの機構をつけるなど拡張していこうと思っています。
なので、共通化しておくとそのような機能拡張をするときに、一箇所(TodoItemEditor)を修正すれば済むという利点があるのです。
まとめ
BlazorでDBのCRUD操作を備えたTodoリストアプリを作りました。
EF Coreを用いたDBアクセスにおける遅延実行の仕組みについて学びました。
Razorコンポーネントをどのように記述するか、そしてどのように動作するかの基本的な考え方についても説明しました。
加えて、Razorコンポーネントを部品化・再利用する方法も学びました。これでBlazorアプリのコードの保守性を高めることができます。
次回は、認証機構Core Identityの使い方を学び、ユーザごとにTodoリストを管理できるようにします。
引き続き、Webアプリ開発とBlazorを一緒に学んでいきましょう!