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

【初心者向け】Blazorアプリ開発入門

Blazorアイキャッチ画像 C#

Blazor Web App (Blazor Server) でシンプルなCRUD機能(作成、読み取り、更新、削除)を持つアプリを作成する方法について解説します。

書籍を管理するアプリを題材に、チュートリアル形式で説明していきます。

対象読者

  • BlazorでCRUD機能のあるアプリを作ってみたい
  • C#の基本文法はある程度理解している

環境

  • Windows 11
  • Visual Studio 2022
  • .NET 8

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

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

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

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

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

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

Componentsフォルダの確認

プロジェクト作成時に自動生成される、Blazor特有のComponentsフォルダの中身を解説します。

Componentsフォルダには、「Razorコンポーネント」と呼ばれる、拡張子が.razor のファイルが置かれています。

Razorコンポーネントは、C#とHTMLを組み合わせてUIを作ることができる再利用可能な部品です。

Razorコンポーネントの基本的な特徴や動きについては、以下の記事を参考にしてください。

【Blazor】コンポーネントの基本を解説 | hiranote

Components/Layoutフォルダ

アプリ全体の見た目(レイアウト)を決めるコンポーネントが格納されたフォルダです。

  • MainLayout.razor: アプリ全体でベースとなる共通のレイアウトが定義されています
  • NavMenu.razor: 画面の左側に表示されるメニューが定義されています

Components/Pagesフォルダ

Razorコンポーネントの中でも、URLを持つコンポーネントを格納するフォルダです。(慣例的にPagesフォルダ内に配置しているだけで、Pagesフォルダ外に配置しても問題ありません)

プロジェクト作成時に「Include sample pages」にチェックを入れていると、Pagesフォルダ内にCounter.razorWeather.razorが追加されます。

  • Error.razor: アプリ実行中にエラーが発生した際に表示されるページです
  • Home.razor: アプリを実行すると最初に表示されるページです
  • Counter.razor: カウントアップするだけのシンプルなサンプルページですが、Blazorの基本的な動作を確認することができます
  • Weather.razor: ページ表示時にデータの中身を描画するサンプルページで、DBから取得したデータを画面に表示する際などに参考になります

_Imports.razor

プロジェクトでよく使用する名前空間やコンポーネントをまとめてインポートすることができるファイルで、ここでインポートした名前空間等は全ての.razorファイルで共通して使うことができます。

App.razor

アプリ起動後に最初に読み込まれるコンポーネントで、アプリの基本構造が定義されています。

次に説明するRoutes コンポーネントの呼び出しや、既定のレンダリングモードの指定が行われています。

Routes.razor

ページのルーティングを行うコンポーネントで、指定されたパスに対応するページをMainLayout.razor@Bodyに埋め込んで表示する役割を担います。

スキャフォールディングとマイグレーション

それでは、CRUD操作に必要なコードを作成していきますが、スキャフォールディング機能を使うとこれらのコードを手軽に自動生成することができます。

スキャフォールディング

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

プロジェクト直下に「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; }
}

それでは追加したクラスを元に、データベースへの接続に必要なファイルや、CRUDに対応したRazorページをスキャフォールディングによって自動生成します。

Componentsフォルダを右クリックして、「追加」→「新規スキャフォールディングアイテム」の順で選択します。

Visual Studio 2022のバージョンが古いとスキャフォールディングに対応していないことがあるので、その場合はバージョンを最新に上げてから実行してください。

左メニューの「Razor コンポーネント」から「Entity Framework を使用する Razorコンポーネント (CRUD)」を選択し、「追加」をクリックします。

Razorコンポーネントの追加ダイアログが表示されます。

「モデルクラス」には先ほど作成したBookクラスを指定し、「DbContextクラス」は「+」ボタンから「追加」で新規作成します。「データベースプロバイダー」は「SQL Server」を指定します(今回はVisual Studioに付属しているSQL ServerのLocalDBを使用します)

最後に「追加」をクリックすると、スキャフォールディングが開始されます。

スキャフォールディングが完了すると、以下の赤枠部分のファイルが自動で追加されます。

また、データベースへの接続に必要な設定がappsettings.jsonや、Program.csに追加されます。

マイグレーション

まずテーブルに登録するための初期データを設定します。

スキャフォールディングによって生成されたDbContextクラスを開き、以下のハイライトされた部分を追加してください。

using BlazorBooks.Models;
using Microsoft.EntityFrameworkCore;

namespace BlazorBooks.Data
{
    public class BlazorBooksContext : DbContext
    {
        public BlazorBooksContext(DbContextOptions<BlazorBooksContext> options)
            : base(options)
        {
        }

        public DbSet<BlazorBooks.Models.Book> Book { get; set; } = default!;

        protected override void OnModelCreating(ModelBuilder 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 }
                );
    }
}

HasData()でテーブルの初期データを設定しています。中身はなんでも構わないのでお好みのデータを登録してください。

初期データが設定できたらマイグレーションを行います。

マイグレーションとは、プログラムで定義したテーブル構造などをDBに自動で反映させることができる仕組みです。

今回であれば、マイグレーションを行うことでBookテーブルと初期データが自動的にLocalDBに反映されます。

ソリューションエクスプローラーのプロジェクト名のすぐ下にある「Connected Services」をダブルクリックして「接続済みサービス」を開いたら、「…」から「移行の追加」をクリックしてください。

「移行の名前」でマイグレーション名を自由に設定できますが、今回はデフォルトのままで「完了」をクリックします。

もしDbContextクラスが見つからないというエラーが出た場合は、ビルドしてから再度「移行の追加」を試してみてください。

マイグレーションが完了するとプロジェクト直下に「Migrations」フォルダが作成されます。

次に、作成されたマイグレーションファイルをもとにDBやテーブルの作成を行います。

接続済みサービス画面の「…」から、「データベースを更新する」を選択してください。

「完了」をクリックすると、LocalDBへの更新が始まります。

データベース更新が完了したら、正しく作成されたかをLocalDBに接続して確認してみます。

確認するにはまず、Visual Studio上部メニューの「表示」から「SQL Server オブジェクトエクスプローラー」を開きます。

生成されたDB名(DbContext名+ランダムID)の中身を開き、Bookテーブルを右クリック→「データの表示」でデータの中身が確認できます。

次に、アプリを実行して動作を確認してみます。

アプリ実行後にアドレスバーに/booksと入力すると、Indexページが表示され、設定した初期データのリストが表示されます。

「Create New」からCreate画面を開き、適当な情報を入力後に「Create」をクリックします。

Indexページにリダイレクトし、登録した情報が画面に表示されます。

EditやDeleteも同様に動作を確認してみてください。

ナビゲーションメニューにリンクを追加

先ほどはアドレスバーにパスを直打ちしましたが、今後はアプリの左側に表示されているナビゲーションメニューから遷移できるようにリンクを追加します。

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

<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属性に、Index.razor@page ディレクティブで指定されている「books」を指定することで、ナビゲーションメニューからIndexページに飛べるようになります。

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

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

最後にアプリを実行し、ナビゲーションメニューから「Index」ページに飛べることを確認してください。

各CRUDコンポーネントの説明

ここまででアプリの基本機能の実装は完了です。

最後にソートやフィルターなどの機能追加を行いますが、その前に、スキャフォールディングで自動生成されたRazorコンポーネントの中身を詳しく説明します。(一部私の方でコメントを付与しています)

Index.razor

Bookテーブルの中身を一覧で表示するページです。データをテーブル形式で表示するためにQuickGridというBlazor標準のコンポーネントを使用している点が特徴的です。

@page "/books"
@using Microsoft.EntityFrameworkCore
@using Microsoft.AspNetCore.Components.QuickGrid
@using BlazorBooks.Models
@using BlazorBooks.Data

@* IAsyncDisposableインターフェースの実装 *@
@implements IAsyncDisposable

@* IDbContextFactoryサービスの注入 *@
@inject IDbContextFactory<BlazorBooks.Data.BlazorBooksContext> DbFactory

<PageTitle>Index</PageTitle>

<h1>Index</h1>

<p>
     @* Createページに遷移 *@
    <a href="books/create">Create New</a>
</p>

@* Bookテーブルの中身を表形式で表示 *@
<QuickGrid Class="table" Items="context.Book">
    <PropertyColumn Property="book => book.Title" />
    <PropertyColumn Property="book => book.Author" />
    <PropertyColumn Property="book => book.Publisher" />
    <PropertyColumn Property="book => book.PublishedYear" />

    <TemplateColumn Context="book">
        <a href="@($"books/edit?id={book.Id}")">Edit</a> |
        <a href="@($"books/details?id={book.Id}")">Details</a> |
        <a href="@($"books/delete?id={book.Id}")">Delete</a>
    </TemplateColumn>
</QuickGrid>

@code {
    private BlazorBooksContext context = default!;

    // コンポーネント初期化時にDbContextを生成してプライベート変数に格納
    protected override void OnInitialized()
    {
        context = DbFactory.CreateDbContext();
    }

    // コンポーネントが破棄される際にDbContextを破棄する
    public async ValueTask DisposeAsync() => await context.DisposeAsync();
}

主なポイントについて簡単に説明します。

  • @pageディレクティブ:コンポーネントのルートを示すもので、必ず指定します。
  • @injectディレクティブ:Program.csでDIコンテナに登録したクラスをコンポーネントに挿入する際に使います。IDbContextFactoryはAddDbContextFactoryメソッドによってDIコンテナに登録済みのため挿入することができます。
    ※参考:ASP.NET Core でのビューへの依存関係の挿入
  • QuickGridコンポーネンQuickGridItemsに指定したクラスの中身をテーブル形式で表示してくれる標準コンポーネントです。PropertyColumnは列を定義する子コンポーネントで、Propertyに表示したいプロパティをラムダ形式で指定します。TemplateColumnも列を定義する子コンポーネントですが、任意の形式でプロパティの中身を表現することができます。
    ※参考:ASP.NET Core BlazorQuickGrid コンポーネント
  • @codeブロックのOnInitializedAsyncは、コンポーネントが初期化されるタイミングで必ず呼び出されるメソッドです。

Details.razor

次に、Bookごとの詳細画面です。

@page "/books/details"
@using Microsoft.EntityFrameworkCore
@using BlazorBooks.Models
@inject IDbContextFactory<BlazorBooks.Data.BlazorBooksContext> DbFactory
@inject NavigationManager NavigationManager

<PageTitle>Details</PageTitle>

<h1>Details</h1>

<div>
    <h2>Book</h2>
    <hr />
    @* 書籍データがDBから取得されるまではロード中であることを表示する *@
    @if (book is null)
    {
        <p><em>Loading...</em></p>
    }
    else {
        <dl class="row">
            <dt class="col-sm-2">Title</dt>
            <dd class="col-sm-10">@book.Title</dd>
            <dt class="col-sm-2">Author</dt>
            <dd class="col-sm-10">@book.Author</dd>
            <dt class="col-sm-2">Publisher</dt>
            <dd class="col-sm-10">@book.Publisher</dd>
            <dt class="col-sm-2">PublishedYear</dt>
            <dd class="col-sm-10">@book.PublishedYear</dd>
        </dl>
        <div>
            <a href="@($"/books/edit?id={book.Id}")">Edit</a> |
            <a href="@($"/books")">Back to List</a>
        </div>
    }
</div>

@code {
    private Book? book;

    // クエリ文字列のIDを保持するプライベート変数
    [SupplyParameterFromQuery]
    private int Id { get; set; }

    protected override async Task OnInitializedAsync()
    {
        using var context = DbFactory.CreateDbContext();

        // Bookテーブル内のIDとクエリ文字列のIDが一致するデータを1件取得してbook変数に格納する
        book = await context.Book.FirstOrDefaultAsync(m => m.Id == Id);

        if (book is null)
        {
            // notfoundページに遷移する
            NavigationManager.NavigateTo("notfound");
        }
    }
}

主な概念について説明します。

  • [SupplyParameterFromQuery]属性:この属性を付与することで、クエリパラメータ(books/details?id=1 でいうid=1の部分)として渡ってきた文字列をプロパティにセットすることができます。
  • NavigationManager.NavigateTo():括弧内に記載されたページへ遷移させることができるメソッドです。ここでは”notfound”というページへ遷移という意味ですが、現在のアプリ内にnotfoundというコンポーネントは存在しないため、ブラウザの404エラー画面が表示されます。実際にアプリを運用する場合は専用のエラー画面を表示するなどの変更が必要です。

Create.razor

次に、書籍を登録するためのページを確認します。

@page "/books/create"
@using Microsoft.EntityFrameworkCore
@using BlazorBooks.Models
@inject IDbContextFactory<BlazorBooks.Data.BlazorBooksContext> DbFactory
@inject NavigationManager NavigationManager

<PageTitle>Create</PageTitle>

<h1>Create</h1>

<h2>Book</h2>
<hr />
<div class="row">
    <div class="col-md-4">

        @* 登録フォーム *@
        <EditForm method="post" Model="Book" OnValidSubmit="AddBook" FormName="create" Enhance>
            @* 属性バリデーションを使用する *@
            <DataAnnotationsValidator />

            @* バリデーション時のエラーメッセージを全て表示する *@
            <ValidationSummary class="text-danger" />
            <div class="mb-3">
                <label for="title" class="form-label">Title:</label> 
                <InputText id="title" @bind-Value="Book.Title" class="form-control" />
                @* バリデーション時のエラーメッセージを個別に表示する *@
                <ValidationMessage For="() => Book.Title" class="text-danger" /> 
            </div>        
            <div class="mb-3">
                <label for="author" class="form-label">Author:</label> 
                <InputText id="author" @bind-Value="Book.Author" class="form-control" /> 
                <ValidationMessage For="() => Book.Author" class="text-danger" /> 
            </div>        
            <div class="mb-3">
                <label for="publisher" class="form-label">Publisher:</label> 
                <InputText id="publisher" @bind-Value="Book.Publisher" class="form-control" /> 
                <ValidationMessage For="() => Book.Publisher" class="text-danger" /> 
            </div>        
            <div class="mb-3">
                <label for="publishedyear" class="form-label">PublishedYear:</label> 
                <InputNumber id="publishedyear" @bind-Value="Book.PublishedYear" class="form-control" /> 
                <ValidationMessage For="() => Book.PublishedYear" class="text-danger" /> 
            </div>        
            <button type="submit" class="btn btn-primary">Create</button>
        </EditForm>
    </div>
</div>

<div>
    <a href="/books">Back to List</a>
</div>

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

    // Createボタン押下時に呼び出される
    private async Task AddBook()
    {
        // フォームの内容をBookテーブルにINSERTする
        using var context = DbFactory.CreateDbContext();
        context.Book.Add(Book);
        await context.SaveChangesAsync();

        // Indexページにリダイレクトする
        NavigationManager.NavigateTo("/books");
    }
}

初めて登場する概念等について解説します。

  • EditForm:標準で用意されている入力フォームのコンポーネントで、HTMLのformタグに対応します。Model属性には対応するモデルクラス名、OnValidSubmit属性はフォーム情報が送信された時(かつ検証エラーが無い時)に実行されるメソッド名を指定します。
  • DataAnnotationsValidator:属性による検証(バリデーション)を行う際に使用するコンポーネントです。今回はモデルクラス(Book.cs)に何も属性を付けていませんが、属性がある場合は入力値の検証が行われます。検証時のエラーメッセージはValidationSummaryValidationMessageが記載されている箇所に表示されます。
  • ValidationSummary, ValidationMessage:いずれも検証時のエラーメッセージを表示するためのコンポーネントで、ValidationSummaryは全てのエラーメッセージをまとめて表示し、ValidationMessageは入力項目ごとのエラーメッセージを個別に表示します。
  • InputText:文字列値を編集するための入力コンポーネントで、HTMLのinputタグに対応します。 @bind-Value属性は、モデルのプロパティをinputタグのvalue属性に双方向でバインド(紐付け)するものです。詳細はドキュメント(ASP.NET Core Blazor フォームの概要)を参照してください。
  • [SupplyParameterFromForm]属性:フォームから送信された情報をプロパティにバインドするために指定します。

Edit.razor

続いてEdit画面です。DetailsとCreateを合わせたような作りになっています。

@page "/books/edit"
@using Microsoft.EntityFrameworkCore
@using BlazorBooks.Models
@inject IDbContextFactory<BlazorBooks.Data.BlazorBooksContext> DbFactory
@inject NavigationManager NavigationManager

<PageTitle>Edit</PageTitle>

<h1>Edit</h1>

<h2>Book</h2>
<hr />
@if (Book is null)
{
    <p><em>Loading...</em></p>
}
else
{
    <div class="row">
        <div class="col-md-4">
            <EditForm method="post" Model="Book" OnValidSubmit="UpdateBook" FormName="edit" Enhance>
                <DataAnnotationsValidator />
                <ValidationSummary />
                <input type="hidden" name="Book.Id" value="@Book.Id" />
                <div class="mb-3">
                    <label for="title" class="form-label">Title:</label>
                    <InputText id="title" @bind-Value="Book.Title" class="form-control" />
                    <ValidationMessage For="() => Book.Title" class="text-danger" />
                </div>
                <div class="mb-3">
                    <label for="author" class="form-label">Author:</label>
                    <InputText id="author" @bind-Value="Book.Author" class="form-control" />
                    <ValidationMessage For="() => Book.Author" class="text-danger" />
                </div>
                <div class="mb-3">
                    <label for="publisher" class="form-label">Publisher:</label>
                    <InputText id="publisher" @bind-Value="Book.Publisher" class="form-control" />
                    <ValidationMessage For="() => Book.Publisher" class="text-danger" />
                </div>
                <div class="mb-3">
                    <label for="publishedyear" class="form-label">PublishedYear:</label>
                    <InputNumber id="publishedyear" @bind-Value="Book.PublishedYear" class="form-control" />
                    <ValidationMessage For="() => Book.PublishedYear" class="text-danger" />
                </div>
                <button type="submit" class="btn btn-primary">Save</button>
            </EditForm>
        </div>
    </div>
}

<div>
    <a href="/books">Back to List</a>
</div>

@code {
    [SupplyParameterFromQuery]
    private int Id { get; set; }

    [SupplyParameterFromForm]
    private Book? Book { get; set; }

    // 初期表示時に呼び出される
    protected override async Task OnInitializedAsync()
    {
        using var context = DbFactory.CreateDbContext();
        Book ??= await context.Book.FirstOrDefaultAsync(m => m.Id == Id);

        if (Book is null)
        {
            NavigationManager.NavigateTo("notfound");
        }
    }

    // Saveボタン押下時に呼び出される
    private async Task UpdateBook()
    {
        using var context = DbFactory.CreateDbContext();
        context.Attach(Book!).State = EntityState.Modified;

        try
        {
            await context.SaveChangesAsync();
        }
        // データが同時に更新された際に発生する例外
        catch (DbUpdateConcurrencyException)
        {
            if (!BookExists(Book!.Id))
            {
                NavigationManager.NavigateTo("notfound");
            }
            else
            {
                throw;
            }
        }

        NavigationManager.NavigateTo("/books");
    }

    private bool BookExists(int id)
    {
        using var context = DbFactory.CreateDbContext();
        return context.Book.Any(e => e.Id == id);
    }
}

HTML部分はCreateとほぼ同じです。

コードブロック内は、初期表示時はDetailsと同様にBookテーブルから概要するデータをBookプロパティに格納し、Saveボタン押下時は入力フォームの内容をBookテーブルにUPDATEしにいきます。

DbUpdateConcurrencyExceptionについてはEntity Framework Coreで更新処理を行う際に発生しうるものでBlazor特有のものではないので詳細は割愛します。気になる方はドキュメントを参照してください。

コンカレンシーの競合の処理 - EF Core
同じデータが Entity Framework Core と同時に更新された場合の競合の管理

Delete.razor

最後に削除ページですが、新しい概念や用語は登場しないので説明は省略します。

@page "/books/delete"
@using Microsoft.EntityFrameworkCore
@using BlazorBooks.Models
@inject IDbContextFactory<BlazorBooks.Data.BlazorBooksContext> DbFactory
@inject NavigationManager NavigationManager

<PageTitle>Delete</PageTitle>

<h1>Delete</h1>

<p>Are you sure you want to delete this?</p>
<div>
    <h2>Book</h2>
    <hr />
    @if (book is null)
    {
        <p><em>Loading...</em></p>
    }
    else {
        <dl class="row">
            <dt class="col-sm-2">Title</dt>
            <dd class="col-sm-10">@book.Title</dd>
        </dl>
        <dl class="row">
            <dt class="col-sm-2">Author</dt>
            <dd class="col-sm-10">@book.Author</dd>
        </dl>
        <dl class="row">
            <dt class="col-sm-2">Publisher</dt>
            <dd class="col-sm-10">@book.Publisher</dd>
        </dl>
        <dl class="row">
            <dt class="col-sm-2">PublishedYear</dt>
            <dd class="col-sm-10">@book.PublishedYear</dd>
        </dl>
        <EditForm method="post" Model="book" OnValidSubmit="DeleteBook" FormName="delete" Enhance>
            <button type="submit" class="btn btn-danger" disabled="@(book is null)">Delete</button> |
            <a href="/books">Back to List</a>
        </EditForm>
    }
</div>

@code {
    private Book? book;

    [SupplyParameterFromQuery]
    private int Id { get; set; }

    protected override async Task OnInitializedAsync()
    {
        using var context = DbFactory.CreateDbContext();
        book = await context.Book.FirstOrDefaultAsync(m => m.Id == Id);

        if (book is null)
        {
            NavigationManager.NavigateTo("notfound");
        }
    }

    private async Task DeleteBook()
    {
        using var context = DbFactory.CreateDbContext();
        context.Book.Remove(book!);
        await context.SaveChangesAsync();
        NavigationManager.NavigateTo("/books");
    }
}

QuickGridコンポーネントを使った追加機能の実装

それでは最後に、Indexページで使われているQuickGridコンポーネントを使って、ソートやページング、フィルター機能を実装します。

ソート機能の実装

QuickGridでは、ソートしたいカラムにSortable="true"を付けるだけで簡単にソート機能が実装できます。

下記はIndex.razorのQuickGrid部分を抜粋したものです。出版年でソートできるように変更しています。

<QuickGrid Class="table" Items="context.Book">
    <PropertyColumn Property="book => book.Title" />
    <PropertyColumn Property="book => book.Author" />
    <PropertyColumn Property="book => book.Publisher" />
    <PropertyColumn Property="book => book.PublishedYear" Sortable="true"/>

    <TemplateColumn Context="book">
        <a href="@($"books/edit?id={book.Id}")">Edit</a> |
        <a href="@($"books/details?id={book.Id}")">Details</a> |
        <a href="@($"books/delete?id={book.Id}")">Delete</a>
    </TemplateColumn>
</QuickGrid>

アプリを実行すると、PublishedYearでソートできることが確認できます。

ページング機能の実装

ページング機能も簡単に実装することができます。

下記は3アイテムごとに改ページするような実装です。

<QuickGrid Class="table" Items="context.Book" Pagination="paginationState">
    <PropertyColumn Property="book => book.Title" />
    <PropertyColumn Property="book => book.Author" />
    <PropertyColumn Property="book => book.Publisher" />
    <PropertyColumn Property="book => book.PublishedYear" Sortable="true" />

    <TemplateColumn Context="book">
        <a href="@($"books/edit?id={book.Id}")">Edit</a> |
        <a href="@($"books/details?id={book.Id}")">Details</a> |
        <a href="@($"books/delete?id={book.Id}")">Delete</a>
    </TemplateColumn>
</QuickGrid>

<Paginator State="paginationState" />

@code {
    private PaginationState paginationState = new() { ItemsPerPage = 3 };

codeブロック内でPagenationState変数を定義し、それをQuickGridPagenatorコンポーネントに渡すだけでページング機能が実現できます。

アプリを実行すると、ページの切り替えができるようになっているはずです。

フィルター機能の実装

最後にフィルター機能を実装します。

今回はタイトル名を任意の文字列で絞り込めるようにします。下記のハイライト部分を追加・変更してください。

<p>
    <input type="search" @bind="titleFilter" @bind:event="oninput" />
</p>

<QuickGrid Class="table" Items="FilteredBooks" Pagination="paginationState">
    <PropertyColumn Property="book => book.Title" />
    <PropertyColumn Property="book => book.Author" />
    <PropertyColumn Property="book => book.Publisher" />
    <PropertyColumn Property="book => book.PublishedYear" Sortable="true" />

    <TemplateColumn Context="book">
        <a href="@($"books/edit?id={book.Id}")">Edit</a> |
        <a href="@($"books/details?id={book.Id}")">Details</a> |
        <a href="@($"books/delete?id={book.Id}")">Delete</a>
    </TemplateColumn>
</QuickGrid>

<Paginator State="paginationState" />

@code {
    private PaginationState paginationState = new() { ItemsPerPage = 3 };
    private BlazorBooksContext context = default!;
    
    private string titleFilter = string.Empty;
    private IQueryable<Book> FilteredBooks => context.Book.Where(b => b.Title.Contains(titleFilter));

codeブロックでは、検索ボックスの文字列が格納されるtitleFilter変数の定義と、検索ボックスの文字列をもとに絞り込んだ書籍データが格納されるFilteredBooks変数を定義しています。

QuickGridには従来のcontext.Bookではなく、絞り込まれた後のFilteredBooksを渡すように変更します。

検索ボックスはtitleFilterとバインドさせます。また、@bind:event="oninput"とすることで、文字を入力するとすぐに結果が反映されるようにしています。

アプリを実行し、下記のように文字列でリストを絞り込めることを確認してみてください。

おわりに

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

最後に、Blazorをさらに学習したい方に向けて、おすすめ書籍や当ブログの参考記事を紹介します。

より実践的なBlazor開発を学びたい

Microsoft社員によるBlazor開発の解説書で、Blazorの基本動作から、.NET 8以降に導入されたBlazor Unitedなどの概念、さらに認証やデプロイのベストプラクティスなど、Blazor開発に必要な知識を網羅的に学ぶことができます。

著:伊藤 稔, 著:大田 一希, 著:小山 崇, 著:辻本 海成, 著:久野 太三, 著:赤間 信幸, 著:井上 章
¥3,168 (2024/11/11 15:39時点 | Amazon調べ)

Azure上にアプリを公開したい

BlazorアプリをAzure App Serviceにデプロイする方法については、下記記事で解説しています。

C#Blazor
hiranote

コメント

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