Webアプリ

【C#、Blazor】Webアプリ開発入門編(5)「Todoアプリ」でデータベースを更新 ~レコードを削除・更新・追加~

前回に引き続き、具体的な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一覧を表示

前回の記事は以下になりますので、ぜひまずは先にこちらをご覧ください。

【C#、Blazor】Webアプリ開発入門編(4)「Todoアプリ」でデータベース作成&データ表示 ~データベース操作のフレームワークを学ぶ~ 今回から具体的なTodoリストアプリ開発を題材として、Blazorの基本機能について紹介します。 初心者がBlazorでWebア...

今回は以下について学びます。

  • 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を一緒に学んでいきましょう!

ABOUT ME
プロ太
プログラミングを勉強している人へ情報を発信していきます! ・情報工学分野で博士(工学)の学位取得 ・言語:C# 、Java、C/C++、Python、JavaScript/TypeScript等 ・仕事は主に上流工程(WF開発・Agile開発、OSS開発経験あり) ・趣味で開発:3Dゲーム、Webアプリ、言語処理系等

ご依頼・ご相談について

プログラミング学習のご相談、お仕事のご依頼については、
こちらのお問い合わせページをご確認ください。