記事内に広告が含まれています

【Blazor入門】シンプルなCRUDアプリを作成する

Blazorアイキャッチ画像 C#

Blazor Web App (Blazor Server) でシンプルなCRUDアプリを作成する方法について解説します。

書籍を管理するアプリを題材に、ポイントを絞って説明していくので、初めてBlazorを触る方でもわかりやすい内容になっているかと思います。

本記事の対象

  • Blazorの基本的な使い方を素早く理解したい
  • C#やHTMLの基礎がある程度わかる

環境

  • Windows 11
  • Visual Studio 2022
  • .NET 8

プロジェクトの作成とデバッグ実行

Visual Studioで新しいプロジェクトの作成画面を開き、「Blazor Web App」を選択してください。

プロジェクト名は任意のものを指定してください。

フレームワークには「.NET 8.0 (長期的なサポート)」を、Interactive render modeには「Server」を選択し、「作成」をクリックします。

プロジェクトが作成できたら、上部の「https」をクリックしてみてください。
プログラムが実行され、ブラウザが起動します。

以下のような画面が表示されれば成功です。

DBの作成

まずデータを保存するためのクラスやDBの作成について説明します。Blazorそのものの内容ではないので簡潔に進めます。

今回はVisual Studioに付属しているSQL ServerのLocal DBを使用します。

※Entity Framework Coreの操作が不安な場合は以下の記事などを参考にしてください。

データモデルクラスとDbContextクラスの追加

まずはテーブルに対応するモデルクラスを追加します。

プロジェクト直下に「Models」フォルダを作成し、その下に「Book.cs」を追加してください。

作成したBook.csに、書籍を管理するための基本的な情報を持つシンプルなクラスを作成します。

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Author { get; set; } = string.Empty;
    public string Publisher { get; set; } = string.Empty;
    public int PublishedYear { get; set; }
}

次にプロジェクト直下に「Data」フォルダを作成し、その下に「BookDbContext.cs」を追加してください。

以下の内容を記述します。(namespaceやusingは適宜自身のプロジェクト名に置き換えてください)

using BlazorCrudSample.Models;
using Microsoft.EntityFrameworkCore;

namespace BlazorCrudSample.Data
{
    public class BookDbContext(DbContextOptions<BookDbContext> options) : DbContext(options)
    {
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<Book>().HasData(
                new Book { Id = 1, Title = "リーダブルコード", Author = "Dustin Boswell, Trevor Foucher", Publisher = "オライリージャパン", PublishedYear = 2012 },
                new Book { Id = 2, Title = "ハリー・ポッターと賢者の石", Author = "J.K.ローリング", Publisher = "静山社", PublishedYear = 1999 },
                new Book { Id = 3, Title = "坂の上の雲", Author = "司馬遼太郎", Publisher = "文藝春秋", PublishedYear = 1999 }
                );
        }

        public DbSet<Book> Books { get; set; }
    }
}

HasData()の部分でテーブルの初期データを設定しています。お好きなデータを登録してください。

まだMicrosoft.EntityFrameworkCore関係のパッケージをインストールしていないのでコンパイルエラーが表示されています。

プロジェクト名を右クリック→「NuGetパッケージの管理」からNuGetパッケージマネージャーを開き、以下の2つのパッケージをインストールしてください。

  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools

マイグレーション

次に、DbContextクラスをアプリに登録するための記述をProgram.csに追記します。

using BlazorCrudSample.Components;
using BlazorCrudSample.Data;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

builder.Services.AddDbContext<BookDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
);

var app = builder.Build();

最後に接続文字列をappsettings.jsonに記載します。
Initial Catalogには作成したいDBの名前を設定してください。

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=Book;Integrated Security=True;Connect Timeout=30;Encrypt=False;Trust Server Certificate=False;Application Intent=ReadWrite;Multi Subnet Failover=False"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

ここまでできたらマイグレーションを実行してDBを作成していきます。

ツール→NuGetパッケージマネージャーからパッケージマネージャーコンソールを開き、下記コマンドを実行してください。
Add-Migration InitialCreate

成功するとMigrations関係のファイルが自動で生成されます。

次に下記コマンドを実行すると、マイグレーションファイルをもとにDBとテーブルが作成されます。

Update-Database

「Done」と表示されれば作成成功です。

上部メニューの「表示」から「SQL Server オブジェクトエクスプローラー」を開くことでテーブルの内容を確認できます。

一覧画面の作成

テーブルとデータが用意できたので、画面に情報を表示してみましょう。

_Imports.razorの設定

まず準備として、「Components」フォルダ内にある「_Imports.razor」を開き、下記の3行のusingディレクティブを追加してください。

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using BlazorCrudSample
@using BlazorCrudSample.Components
@using BlazorCrudSample.Data
@using BlazorCrudSample.Models
@using Microsoft.EntityFrameworkCore;

_Imports.razorに書かれたusingディレクティブは、同階層下の全てのファイルに適用されるため、ファイルごとにusingを指定する必要がなくなります。

一覧ページの作成

次に、Components > Pages フォルダの下に「Book」フォルダを作成し、その中に「List.razor」ファイルを追加します。(Razorファイルの追加は「追加」→「Razorコンポーネント」)

作成したList.razorに下記のコードを記述します。

@page "/books"
@inject BookDbContext Context
@attribute [StreamRendering]

<PageTitle>Books</PageTitle>
<h3>Books</h3>

@* 書籍のデータが取得できるまではローディング中のメッセージを表示 *@
@if (books.Count == 0)
{
    <p><em>Loading ...</em></p>
}
else
{
    @* 取得した書籍データを表形式で表示 *@
    <table class="table">
        <thead>
            <tr>
                <th>タイトル</th>
                <th>著者</th>
                <th>出版社</th>
                <th>出版年</th>
                <th></th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            @foreach (var book in books)
            {
                <tr>
                    <td>@book.Title</td>
                    <td>@book.Author</td>
                    <td>@book.Publisher</td>
                    <td>@book.PublishedYear</td>
                    <td>編集</td>
                    <td>削除</td>
                </tr>
            }
        </tbody>
    </table>
}

<button class="btn btn-primary">追加</button>

@code {
    private List<Book> books = new();

    // コンポーネント初期化時にBooksテーブルから全データを取得してプライベート変数に格納
    protected override async Task OnInitializedAsync()
    {
        books = await Context.Books.ToListAsync();
    }
}

Blazor(razor)特有のポイントについて簡単に説明します。

  • @page ディレクティブ:コンポーネントのルートを示すもので、必ず指定します。
  • @inject ディレクティブ:Program.csでDIコンテナに登録したクラスをコンポーネントに挿入するために使います。BookDbContextクラスはAddDbContextメソッドによってDIコンテナに登録済みのため挿入することができます。
    ※参考:ASP.NET Core でのビューへの依存関係の挿入
  • @attribute [StreamRendering]:.NET 8からの新機能で、時間がかかる処理の前に一度画面のレンダリング(描画)を行うことができます。今回ではDBからのデータ取得が完了するまではLoading…と表示され、データが取得できたら全ての画面がレンダリングされます。
    ※参考:ASP.NET Core Razor コンポーネントのレンダリング
  • @codeブロックのOnInitializedAsyncは、コンポーネントが初期化されるタイミングで必ず呼び出されるメソッドです。

なお、コンポーネントの基本的な仕組みや動きについては別記事で詳しく解説しているので、詳しく知りたい方は参照してみてください。

ナビゲーションバーにリンク追加

次に、アプリの左側に表示されているナビゲーションバーにリンクを追加して、List.razorに遷移できるようにします。

Components > Layout フォルダ内の「NavMenu.cs」に下記コードを追記してください。

<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">BlazorCrudSample</a>
    </div>
</div>

<input type="checkbox" title="Navigation menu" class="navbar-toggler" />

<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
            </NavLink>
        </div>

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="counter">
                <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
            </NavLink>
        </div>

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="weather">
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
            </NavLink>
        </div>

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="books">
                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-book" viewBox="0 0 16 16">
                    <path d="M1 2.828c.885-.37 2.154-.769 3.388-.893 1.33-.134 2.458.063 3.112.752v9.746c-.935-.53-2.12-.603-3.213-.493-1.18.12-2.37.461-3.287.811V2.828zm7.5-.141c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492V2.687zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783z" />
                </svg>
                Books
            </NavLink>
        </div>
    </nav>
</div>

NavLinkhref属性に、List.razorの@page ディレクティブで指定した「books」を指定するだけでOKです。

なお、<svg>タグで囲まれた部分は本のアイコンを表示する箇所で、Bootstrap Iconsから取得しています。

参考:https://icons.getbootstrap.jp/icons/book/

お好きなアイコンを探して使ってみてください。

アプリを実行すると左メニューに「Books」が表示されるのでクリックしてみましょう。

ページを開くと、Booksテーブルの中身が画面に表示されるはずです。
(編集や追加ボタンはまだリンク先を設定していないのでクリックしても何も起きません)

登録ページの作成

次に書籍を登録するためのページを作成します。

Pages > Book フォルダ直下に「Edit.razor」を追加し、下記のコードを記述してください。

@page "/edit"
@inject BookDbContext Context

<PageTitle>書籍の登録</PageTitle>
<h3>書籍の登録</h3>

<EditForm Model="Book" OnSubmit="OnSubmit" FormName="EditForm">
    <div>
        <label for="title">タイトル</label>
        <InputText id="title" @bind-Value="Book.Title" class="form-control" />
    </div>
    <div>
        <label for="author">著者</label>
        <InputText id="author" @bind-Value="Book.Author" class="form-control" />
    </div>
    <div>
        <label for="publisher">出版社</label>
        <InputText id="publisher" @bind-Value="Book.Publisher" class="form-control" />
    </div>
    <div>
        <label for="publishedYear">出版年</label>
        <InputNumber id="publishedYear" @bind-Value="Book.PublishedYear" class="form-control" />
    </div>

    <br />
    <button type="submit" class="btn btn-primary">保存</button>
</EditForm>

<p>@message</p>

@code {
    // フォームから送信された情報を保持するプロパティ
    [SupplyParameterFromForm]
    private Book Book { get; set; } = new();

    private string message = string.Empty;

    // フォーム送信時に呼び出される
    private async Task OnSubmit()
    {
        // フォームの内容をBooksテーブルに登録する
        Context.Books.Add(Book);
        var result = await Context.SaveChangesAsync();

        message = (result == 1) ? "登録しました" : "登録に失敗しました";
    }
}

ポイントを解説します。

  • EditForm:標準で用意されている入力フォームのコンポーネントで、HTMLのformタグに対応します。Model属性には対応するモデルクラス名、OnSubmit属性はフォーム情報が送信された時に実行されるメソッド名を指定します。
  • InputText:文字列値を編集するための入力コンポーネントで、HTMLのinputタグに対応します。 @bind-Value属性は、モデルのプロパティをinputタグのvalue属性に双方向でバインド(紐付け)するものです。詳細はドキュメント(ASP.NET Core Blazor フォームの概要)を参照してください。
  • [SupplyParameterFromForm] 属性:フォームから送信された情報をプロパティにバインドするために指定します。
  • 「保存」をクリックするとOnSubmitメソッドが呼ばれ、Booksテーブルにデータが登録できたら画面に「登録しました」と表示されます。

※本来であれば入力内容のバリデーションを行うべきですが、今回は省略しています。

アプリを実行し、実際に書籍を登録してみてください。

編集機能の作成

続けて編集機能を作成します。新たにページを作成するのではなく、既存の登録ページの中に処理を追加していきます。

色付きの箇所が今回追加するコードです。

@page "/edit"
@page "/edit/{Id:int}"
@inject BookDbContext Context

@if (Id is null)
{
    <PageTitle>書籍の登録</PageTitle>
    <h3>書籍の登録</h3>
}
else
{
    <PageTitle>編集</PageTitle>
    <h3>編集</h3>
}

<EditForm Model="Book" OnSubmit="OnSubmit" FormName="BookForm">
     <div>
         <label for="title">タイトル</label>
         <InputText id="title" @bind-Value="Book.Title" class="form-control" />
     </div>
    <div>
        <label for="author">著者</label>
        <InputText id="author" @bind-Value="Book.Author" class="form-control" />
    </div>
     <div>
         <label for="publisher">出版社</label>
         <InputText id="publisher" @bind-Value="Book.Publisher" class="form-control" />
     </div>
     <div>
        <label for="publishedYear">出版年</label>
        <InputNumber id="publishedYear" @bind-Value="Book.PublishedYear" class="form-control" />
     </div>

    <br />
     <button type="submit" class="btn btn-primary">保存</button>
 </EditForm>

 <p>@message</p>

@code {
    [Parameter]
    private int? Id { get; set; } = null;

    // フォームから送信された情報を保持するプロパティ
    [SupplyParameterFromForm]
    private Book Book { get; set; } = new();

    private string message = string.Empty;

    // コンポーネント初期化時とパラメータ(今回はId)が変更された時に呼び出される
    protected override async Task OnParametersSetAsync()
    {
        if (Id is not null)
        {
            // 書籍情報が取得できた場合はそれぞれのプロパティに値を設定する
            var book = await Context.Books.FindAsync(Id);
            if (book is not null)
            {
                Book.Title = book.Title;
                Book.Author = book.Author;
                Book.Publisher = book.Publisher;
                Book.PublishedYear = book.PublishedYear;
            }
        }
    }

    // フォーム送信時に呼び出される
    private async Task OnSubmit()
    {
        if (Id is not null)
        {
            await Update();
        }
        else
        {
            await Create();
        }
    }

    // 更新処理
    private async Task Update()
    {
        // 書籍情報が取得できた場合はそれぞれのプロパティの値をフォームの内容で更新する
        var book = await Context.Books.FindAsync(Id);
        if (book is not null)
        {
            book.Title = Book.Title;
            book.Author = Book.Author;
            book.Publisher = Book.Publisher;
            book.PublishedYear = Book.PublishedYear;

            var result = await Context.SaveChangesAsync();

            message = (result == 1) ? "更新しました" : "更新に失敗しました";
        }
    }

    // 登録処理
    private async Task Create()
    {
        // フォームの内容をBooksテーブルに登録する
        Context.Books.Add(Book);
        var result = await Context.SaveChangesAsync();

        message = (result == 1) ? "登録しました" : "登録に失敗しました";
    }
}

タイトル部分や各入力項目の初期表示は、パスにIdパラメータが指定されているかどうかで編集か登録かを切り替えています。

その他、主なポイントを解説します。

  • @page "/edit/{Id:int}":他のコンポーネントからパラメータ(ルートパラメータという)を受け取るには、{}の中にパラメータを記述します。コロンの後に制約を書くことができ、今回のようにintとすると、int型のパラメータのみを受け取れるようになります。詳細はドキュメント(ルート制約)を参照してください。
  • [Parameter] 属性:ルートパラメータを受け取るためのプロパティに付与します。
  • OnParametersSetAsync:コメントにも記載があるように、コンポーネントが初期化されるタイミングとパラメータ(今回はId)が変更された時に呼び出されるメソッドです。

最後にList.razorから編集ページへのリンクも忘れずに追加しておきます。

<!-- 省略 -->

    @* 取得した書籍データを表形式で表示 *@
    <table class="table">
        <thead>
            <tr>
                <th>タイトル</th>
                <th>著者</th>
                <th>出版社</th>
                <th>出版年</th>
                <th></th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            @foreach (var book in books)
            {
                <tr>
                    <td>@book.Title</td>
                    <td>@book.Author</td>
                    <td>@book.Publisher</td>
                    <td>@book.PublishedYear</td>
                    <td><a href="edit/@book.Id">編集</a></td>
                    <td>削除</td>
                </tr>
            }
        </tbody>
    </table>
}

<!-- 省略 -->

編集リンクをクリックすると該当のIDの編集画面に遷移し、書籍の情報が初期表示されることや、編集ができることを確認してみてください。

削除ページの追加

最後に削除ページを追加します。

Pages > Book フォルダ直下に「Delete.razor」を追加してください。

@page "/delete/{Id:int}"
@inject BookDbContext Context
@inject NavigationManager NavigationManager

<PageTitle>書籍の削除</PageTitle>
<h3>書籍の削除</h3>

<EditForm Model="Book" OnSubmit="OnSubmit" FormName="DeleteForm">
    <div>
        <label for="title">タイトル</label>
        <InputText id="title" @bind-Value="Book.Title" class="form-control" disabled />
    </div>
    <div>
        <label for="author">著者</label>
        <InputText id="author" @bind-Value="Book.Author" class="form-control" disabled />
    </div>
    <div>
        <label for="publisher">出版社</label>
        <InputText id="publisher" @bind-Value="Book.Publisher" class="form-control" disabled />
    </div>
    <div>
        <label for="publishedYear">出版年</label>
        <InputNumber id="publishedYear" @bind-Value="Book.PublishedYear" class="form-control" disabled />
    </div>

    <br />
    <button type="submit" class="btn btn-danger">削除</button>
</EditForm>

@code {
    [Parameter]
    public int? Id { get; set; } = null;

    [SupplyParameterFromForm]
    private Book Book { get; set; } = new();

    // 初期化時にBookがnullの場合はnewする
    protected override void OnInitialized() => Book ??= new();

    protected override async Task OnParametersSetAsync()
    {
        if (Id is not null)
        {
            var book = await Context.Books.FindAsync(Id);
            if (book is not null)
            {
                Book.Title = book.Title;
                Book.Author = book.Author;
                Book.Publisher = book.Publisher;
                Book.PublishedYear = book.PublishedYear;
            }
        }
    }

    private async Task OnSubmit()
    {
        var book = await Context.Books.FindAsync(Id);
        if (book is not null)
        {
            Context.Remove(book);
            await Context.SaveChangesAsync();

            // 一覧ページに移動する
            NavigationManager.NavigateTo("/books");
        }
    }
}

Edit.razorとほぼ同じ内容ですが、フォームの各入力項目にdisabled属性を付与して編集できないようにしていたり、削除処理後に自動でList.razorに遷移している点などが異なります。

NavigationManagerの使い方についてはドキュメント(URI およびナビゲーション状態ヘルパー)を参照してください。

List.razorからDelete.razorへリンクすれば完成です。

<!-- 省略 -->

    @* 取得した書籍データを表形式で表示 *@
    <table class="table">
        <thead>
            <tr>
                <th>タイトル</th>
                <th>著者</th>
                <th>出版社</th>
                <th>出版年</th>
                <th></th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            @foreach (var book in books)
            {
                <tr>
                    <td>@book.Title</td>
                    <td>@book.Author</td>
                    <td>@book.Publisher</td>
                    <td>@book.PublishedYear</td>
                    <td><a href="edit/@book.Id">編集</a></td>
                    <td><a href="delete/@book.Id">削除</a></td>
                </tr>
            }
        </tbody>
    </table>
}

<!-- 省略 -->

アプリを動かして書籍情報が削除できることを確認してみてください。

おわりに

Blazor Web Appを使った基本的なCRUDアプリの作り方を説明しました。

今回作ったアプリがBlazorの理解を深めるきっかけになれば幸いです。

【おすすめ書籍】

C#Blazor
hiranote

コメント

タイトルとURLをコピーしました