LoginSignup
26
29

More than 1 year has passed since last update.

ASP.NET Core Blazor WebAssembly と Web API と Entity Framework Core で SQL Server のデータを取得したり追加したり更新したり削除したりする [.NET 6 版]

Posted at

はじめに

2022 年 11 月に .NET 6 と Visual Studio 2022 がリリースされました。ということで .NET 6 版の Blazor WebAssembly / ASP.NET Core Web API / Entity Framework を使って CRUD アプリケーションを作っていきたいと思います。

(以前書いたこれの .NET 6 版です)

完成イメージ

Animation.gif

環境

  • Windows 10 Pro
  • Microsoft Visual Studio Community 2022 (64 ビット)
  • .NET 6.0

プロジェクト作成

Visual Studio を起動して「新しいプロジェクトの作成」を選択します。

image.png

「Blazor WebAssembly アプリ」を選択して「次へ」。

image.png

プロジェクト名とソリューション名を「BlazorApps」、任意の場所を選択して「次へ」。

image.png

フレームワークに .NET 6.0を選択し、「HTTPS 用の構成」と「ASP.NET Core でホストされた」を有効にして「作成」。

image.png

以下のようにプロジェクトが3つ生成されます。Client がフロントエンド、Server がバックエンドで、Shared には共通で利用するクラスを配置します。

image.png

プロジェクトファイルを見てみましょう。

BlazorApp.Client.csproj
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0" PrivateAssets="all" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\Shared\BlazorApp.Shared.csproj" />
  </ItemGroup>

</Project>

BlazorApp.Server.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="6.0.0" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\Client\BlazorApp.Client.csproj" />
    <ProjectReference Include="..\Shared\BlazorApp.Shared.csproj" />
  </ItemGroup>


</Project>

BlazorApp.Shared.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <SupportedPlatform Include="browser" />
  </ItemGroup>
</Project>

.NET 6.0 では既定で Null 許容参照型と ImplicitUsings が有効になります。

<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>

データベース作成(コードファースト)

データベース用のモデルクラスを作成していきます。あとでクライアント側でも同じクラスを使うので、BlazorApp.Shared プロジェクト直下に Models フォルダーを作成して、その配下に Book.cs を作成します。

BlazorApp.Shared/Models/Book.cs
namespace BlazorApp.Shared.Models
{
    public class Book
    {
        public int BookId { get; set; }
        public string Title { get; set; }
        public string Author { get; set; }

        public Book(int bookId, string title, string author)
        {
            BookId = bookId;
            Title = title;
            Author = author;
        }
    }
}

パッケージマネージャーコンソールから Entity Framework Core のパッケージをインストールします。

Install-Package -ProjectName BlazorApp.Server -Id Microsoft.EntityFrameworkCore.SqlServer
Install-Package -ProjectName BlazorApp.Server -Id Microsoft.EntityFrameworkCore.Tools

次にエンティティクラスとテーブルをマッピングするためのデータベースコンテキストクラスを作成していきます。BlazorApp.Server プロジェクト直下に Data フォルダーを作成し、AppDbContext クラスを作成します。

BlazorApp.Server/Data/AppDbContext.cs
using BlazorApp.Shared.Models;
using Microsoft.EntityFrameworkCore;

namespace BlazorApp.Server.Data
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions options) : base(options)
        {
        }

        public DbSet<Book> Books => Set<Book>();
    }
}

BlazorApp.Server プロジェクトの Program.cs にコンテクストクラスをサービスとして登録します。

BlazorApp.Server/Program.cs
using BlazorApp.Server.Data;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); //追加

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseWebAssemblyDebugging();
}
else
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseBlazorFrameworkFiles();
app.UseStaticFiles();

app.UseRouting();


app.MapRazorPages();
app.MapControllers();
app.MapFallbackToFile("index.html");

app.Run();

appsettings.json に接続文字列を追加します。下記では LocalDb に BlazorAppDb という名前のデータベースを指定しています。

BlazorApp.Server/appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=BlazorAppDb;Trusted_Connection=True;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

パッケージマネージャーコンソールで以下のコマンドを実行します。(Migrations フォルダーと Migration クラスが作成される)

Add-Migration -Name Initial

続けて以下のコマンドを実行。(データベースとテーブルが作成される)

Update-Database

SQL Server Managment Studio か何かで以下のクエリを実行してレコードを追加しておきます。

INSERT INTO Books (Title, Author) VALUES (N'たったひとつの冴えたやりかた',N'ジェイムズ・ティプトリー・ジュニア');
INSERT INTO Books (Title, Author) VALUES (N'アンドロイドは電気羊の夢を見るか?',N'フィリップ・K・ディック');
INSERT INTO Books (Title, Author) VALUES (N'夏への扉',N'ロバート・A. ハインライン');
INSERT INTO Books (Title, Author) VALUES (N'幼年期の終り',N'アーサー C クラーク');
INSERT INTO Books (Title, Author) VALUES (N'われはロボット',N'アイザック・アシモフ');

Read(全件)

バックエンド

全件取得用の Web API を準備します。BlazorApp.Server プロジェクトの Controllers フォルダーに BooksController コントローラーを追加します。

image.png

image.png

BlazoreApp.Server/Controllers/BooksController.cs
using BlazorApp.Server.Data;
using BlazorApp.Shared.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace BlazorApp.Server.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BooksController : ControllerBase
    {
        private readonly AppDbContext context;

        public BooksController(AppDbContext context)
        {
            this.context = context;
        }

        [HttpGet]
        public async Task<ActionResult<List<Book>>> ListAsync()
        {
            var books = await context.Books.ToListAsync();
            return Ok(books);
        }
    }
}

デバッグ実行して /api/books にアクセスして、以下の情報が取得できることを確認しておきます。

[
    {
        "bookId": 1,
        "title": "たったひとつの冴えたやりかた",
        "author": "ジェイムズ・ティプトリー・ジュニア"
    },
    {
        "bookId": 2,
        "title": "アンドロイドは電気羊の夢を見るか?",
        "author": "フィリップ・K・ディック"
    },
    {
        "bookId": 3,
        "title": "夏への扉",
        "author": "ロバート・A. ハインライン"
    },
    {
        "bookId": 4,
        "title": "幼年期の終り",
        "author": "アーサー C クラーク"
    },
    {
        "bookId": 5,
        "title": "われはロボット",
        "author": "アイザック・アシモフ"
    }
]

Ant Design Blazor インストール

今回は UI コンポーネントライブラリを使ってフロントエンドを開発していきます。ライブラリは Awesome Blazor によると一番人気がありそうな Ant Design Blazor を使います。

パッケージマネージャーから以下のコマンドを実行して AntDesign をインストールします。

Install-Package -ProjectName BlazorApp.Client -Id AntDesign

BlazorApp.Client プロジェクトの Program.cs にサービスを登録します。

BlazorApp.Client/Program.cs
using BlazorApp.Client;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddAntDesign(); //追加

await builder.Build().RunAsync();

BlazorApp.Client プロジェクト wwwroot/index.html に CSS と JavaScript を追加します。

BlazorApp.Client/wwwroot/index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>BlazorApp</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="BlazorApp.Client.styles.css" rel="stylesheet" />
    <link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" /><!--追加-->
</head>

<body>
    <div id="app">Loading...</div>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>
    <script src="_content/AntDesign/js/ant-design-blazor.js"></script><!--追加-->
</body>

</html>

BlazorApp.Client プロジェクト直下の _imports.razorAntDesign を追加します。

BlazorApp.Client/_imports.razor
@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 Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using BlazorApp.Client
@using BlazorApp.Client.Shared
@using AntDesign

BlazorApp.Client プロジェクト直下の App.razor<AntContainer /> を追加します。

BlazorApp.Client/App.razor
<Router AppAssembly="@typeof(App).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
    <NotFound>
        <PageTitle>Not found</PageTitle>
        <LayoutView Layout="@typeof(MainLayout)">
            <p role="alert">Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

<AntContainer /><!--追加-->

Ant Design Blazor を使う準備は以上です。

フロントエンド

BlazorApp.Client プロジェクトの Pages フォルダー配下に BookList Razor コンポーネントを追加します。

image.png

BookList コンポーネントを以下のように編集します。

BlazorApp.Client/Pages/BookList.razor
@page "/booklist"
@using BlazorApp.Shared.Models
@inject HttpClient Http

@if (books == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <Table TItem="Book" DataSource="@books">
        <Column @bind-Field="@context.BookId" Title="ID" />
        <Column @bind-Field="@context.Title" />
        <Column @bind-Field="@context.Author" />
    </Table>
}

@code {
    private List<Book>? books;

    protected override async Task OnInitializedAsync()
    {
        books = await Http.GetFromJsonAsync<List<Book>>("api/books");
    }
}

コンポーネントを開く際に OnInitializedAsycn() メソッドが呼ばれるため、そこで先ほど作った API を呼んで、テーブルに書き出すという流れです。

このままだと動線がないので、Shared/NavMenu.razor のリンクに BookList へのリンクを追加しておきます。

BlazorApp.Client/Shared/NavMenu.razor
<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">BlazorApp</a>
        <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
            <span class="navbar-toggler-icon"></span>
        </button>
    </div>
</div>

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="counter">
                <span class="oi oi-plus" aria-hidden="true"></span> Counter
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="fetchdata">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
            </NavLink>
        </div>
        <!--追加-->
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="booklist">
                <span class="oi oi-book" aria-hidden="true"></span> Book list
            </NavLink>
        </div>
        <!--追加ここまで-->
    </nav>
</div>

@code {
    private bool collapseNavMenu = true;

    private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    private void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}

ここまで出来たら実行して以下のように表示されれば OK です。

image.png

Read(1 件)

バックエンド

1 件取得用の Web API を準備します。BooksController.csGetAsync メソッドを追加します。

BlazoreApp.Server/Controllers/BooksController.cs
using BlazorApp.Server.Data;
using BlazorApp.Shared.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace BlazorApp.Server.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BooksController : ControllerBase
    {
        private readonly AppDbContext context;

        public BooksController(AppDbContext context)
        {
            this.context = context;
        }

        [HttpGet]
        public async Task<ActionResult<List<Book>>> ListAsync()
        {
            var books = await context.Books.ToListAsync();
            return Ok(books);
        }

        //追加
        [HttpGet("{id}")]
        public async Task<ActionResult<Book>> GetAsync(int id)
        {
            var book = await context.Books.SingleOrDefaultAsync(b => b.BookId.Equals(id));

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

            return Ok(book);
        }
    }
}

フロントエンド

BookDetail コンポーネントを追加します。(Detail と言っても今回は一覧と内容同じだけど)

BlazorApp.Client/Pages/BookDetail.razor
@using BlazorApp.Shared.Models

@if (Book == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <Descriptions>
        <DescriptionsItem Title="ID" Span="3">@Book.BookId</DescriptionsItem>
        <DescriptionsItem Title="Title" Span="3">@Book.Title</DescriptionsItem>
        <DescriptionsItem Title="Author" Span="3">@Book.Author</DescriptionsItem>
    </Descriptions>
}

@code {
    [Parameter]
    public Book? Book { get; set; }
}

ID をクリックすると BookDetail コンポーネントをモーダルに表示するように BookList コンポーネントを修正します。

BlazorApp.Client/Pages/BookList.razor
@page "/booklist"
@using BlazorApp.Shared.Models
@inject HttpClient Http

@if (books == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <Table TItem="Book" DataSource="@books">
        <Column @bind-Field="@context.BookId" Title="ID">
            <Button Type="link" OnClick="() => OpenDetailModal(context.BookId)">@context.BookId</Button>
        </Column>
        <Column @bind-Field="@context.Title" />
        <Column @bind-Field="@context.Author" />
    </Table>

    <Modal Title="Detail" Visible="@showDetailModal" OnCancel="@CloseModal" Footer="@null">
        <BookDetail Book="@book"></BookDetail>
    </Modal>
}

@code {
    private List<Book>? books;
    private Book? book;
    private bool showDetailModal = false;

    protected override async Task OnInitializedAsync()
    {
        books = await Http.GetFromJsonAsync<List<Book>>("api/books");
    }

    public async Task OpenDetailModal(int id)
    {
        book = await Http.GetFromJsonAsync<Book>($"api/books/{id}");
        showDetailModal = true;
    }

    public void CloseModal()
    {
        showDetailModal = false;
    }
}

こんな感じになります。

Animation.gif

Create

バックエンド

BooksControllerCreate メソッドを追加します。

BlazoreApp.Server/Controllers/BooksController.cs
using BlazorApp.Server.Data;
using BlazorApp.Shared.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace BlazorApp.Server.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BooksController : ControllerBase
    {
        private readonly AppDbContext context;

        public BooksController(AppDbContext context)
        {
            this.context = context;
        }

        [HttpGet]
        public async Task<ActionResult<List<Book>>> ListAsync()
        {
            var books = await context.Books.ToListAsync();
            return Ok(books);
        }

        [HttpGet("{id}")]
        public async Task<ActionResult<Book>> GetAsync(int id)
        {
            var book = await context.Books.SingleOrDefaultAsync(b => b.BookId.Equals(id));

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

            return Ok(book);
        }

        //追加
        [HttpPost]
        public async Task<ActionResult<Book>> CreateAsync(Book book)
        {
            if (book.BookId != 0)
            {
                return BadRequest();
            }

            context.Books.Add(book);
            await context.SaveChangesAsync();

            return CreatedAtAction("Get", new { id = book.BookId }, book);
        }
    }
}

フロントエンド

BookCreate コンポーネントを追加します。

BlazorApp.Client/Pages/BookCreate.razor
@using BlazorApp.Shared.Models
@using System.Text.Json
@inject HttpClient Http
@inject NotificationService Notice

@if (Book == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <Form Model="@Book" OnFinish="Add" LabelColSpan="4" WrapperColSpan="20">
    <FormItem Label="Title">
        <Input @bind-Value="@context.Title" />
    </FormItem>
    <FormItem Label="Author">
        <Input @bind-Value="@context.Author" />
    </FormItem>
    <FormItem WrapperColSpan="24" Style="text-align: center">
        <Button Type="primary" HtmlType="submit">Add</Button>
    </FormItem>
    </Form>
}

@code {
    [Parameter]
    public Book? Book { get; set; }

    [Parameter]
    public EventCallback Callback { get; set; }

    private async Task Add()
    {
        var response = await Http.PostAsJsonAsync("api/books", Book);

        var createdBookJson = await response.Content.ReadAsStringAsync();

        await Callback.InvokeAsync();
        await Notice.Open(new NotificationConfig()
        {
            Message = "New Book is Created!",
            Description = createdBookJson
        });
    }
}

ボタンをクリックすると BookCreate コンポーネントをモーダルに表示するように BookList コンポーネントを修正します。

BlazorApp.Client/Pages/BookList.razor
@page "/booklist"
@using BlazorApp.Shared.Models
@inject HttpClient Http

@if (books == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <Button Type="primary" OnClick="OpenCreateModal" Block>New Book</Button>
    <Divider></Divider>

    <Table TItem="Book" DataSource="@books">
        <Column @bind-Field="@context.BookId" Title="ID">
            <Button Type="link" OnClick="() => OpenDetailModal(context.BookId)">@context.BookId</Button>
        </Column>
        <Column @bind-Field="@context.Title" />
        <Column @bind-Field="@context.Author" />
    </Table>

    <Modal Title="Detail" Visible="@showDetailModal" OnCancel="@CloseModal" Footer="@null">
        <BookDetail Book="@book"></BookDetail>
    </Modal>

    <Modal Title="Create" Visible="@showCreateModal" OnCancel="@CloseModal" Footer="@null">
        <BookCreate Book="@book" Callback="@OnCallback"></BookCreate>
    </Modal>
}

@code {
    private List<Book>? books;
    private Book? book;
    private bool showDetailModal = false;
    private bool showCreateModal = false;

    protected override async Task OnInitializedAsync()
    {
        books = await Http.GetFromJsonAsync<List<Book>>("api/books");
    }

    public async Task OpenDetailModal(int id)
    {
        book = await Http.GetFromJsonAsync<Book>($"api/books/{id}");
        showDetailModal = true;
    }

    public void CloseModal()
    {
        showDetailModal = false;
        showCreateModal = false;
    }

    private void OpenCreateModal()
    {
        book = new(0, "", "");
        showCreateModal = true;
    }

    private async Task OnCallback()
    {
        books = await Http.GetFromJsonAsync<List<Book>>("api/books");
        showCreateModal = false;
    }
}

こんな感じになります。

Animation.gif

Delete

バックエンド

BooksControllerDelete メソッドを追加します。

BlazoreApp.Server/Controllers/BooksController.cs
using BlazorApp.Server.Data;
using BlazorApp.Shared.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace BlazorApp.Server.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BooksController : ControllerBase
    {
        private readonly AppDbContext context;

        public BooksController(AppDbContext context)
        {
            this.context = context;
        }

        [HttpGet]
        public async Task<ActionResult<List<Book>>> ListAsync()
        {
            var books = await context.Books.ToListAsync();
            return Ok(books);
        }

        [HttpGet("{id}")]
        public async Task<ActionResult<Book>> GetAsync(int id)
        {
            var book = await context.Books.SingleOrDefaultAsync(b => b.BookId.Equals(id));

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

            return Ok(book);
        }

        [HttpPost]
        public async Task<ActionResult<Book>> CreateAsync(Book book)
        {
            if (book.BookId != 0)
            {
                return BadRequest();
            }

            context.Books.Add(book);
            await context.SaveChangesAsync();

            return CreatedAtAction("Get", new { id = book.BookId }, book);
        }

        [HttpDelete("{id}")]
        public async Task<ActionResult> DeleteAsync(int id)
        {
            var book = await context.Books.SingleOrDefaultAsync(b => b.BookId.Equals(id));

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

            context.Books.Remove(book);
            await context.SaveChangesAsync();

            return NoContent();
        }
    }
}

フロントエンド

ボタンをクリックするとアイテムを削除するように BookList コンポーネントを修正します。

BlazorApp.Client/Pages/BookList.razor
@page "/booklist"
@using BlazorApp.Shared.Models
@inject HttpClient Http

@if (books == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <Button Type="primary" OnClick="OpenCreateModal" Block>New Book</Button>
    <Divider></Divider>

    <Table TItem="Book" DataSource="@books">
        <Column @bind-Field="@context.BookId" Title="ID">
            <Button Type="link" OnClick="() => OpenDetailModal(context.BookId)">@context.BookId</Button>
        </Column>
        <Column @bind-Field="@context.Title" />
        <Column @bind-Field="@context.Author" />
        <ActionColumn>
            <Popconfirm Title="本当に削除しますか?" OkText="はい" CancelText="いいえ" OnConfirm="() => Delete(context.BookId)">
                <Button Danger>Delete</Button>
            </Popconfirm>
        </ActionColumn>
    </Table>

    <Modal Title="Detail" Visible="@showDetailModal" OnCancel="@CloseModal" Footer="@null">
        <BookDetail Book="@book"></BookDetail>
    </Modal>

    <Modal Title="Create" Visible="@showCreateModal" OnCancel="@CloseModal" Footer="@null">
        <BookCreate Book="@book" Callback="@OnCallback"></BookCreate>
    </Modal>
}

@code {
    private List<Book>? books;
    private Book? book;
    private bool showDetailModal = false;
    private bool showCreateModal = false;

    protected override async Task OnInitializedAsync()
    {
        books = await Http.GetFromJsonAsync<List<Book>>("api/books");
    }

    public async Task OpenDetailModal(int id)
    {
        book = await Http.GetFromJsonAsync<Book>($"api/books/{id}");
        showDetailModal = true;
    }

    public void CloseModal()
    {
        showDetailModal = false;
        showCreateModal = false;
    }

    private void OpenCreateModal()
    {
        book = new(0, "", "");
        showCreateModal = true;
    }

    private async Task OnCallback()
    {
        books = await Http.GetFromJsonAsync<List<Book>>("api/books");
        showCreateModal = false;
    }

    public async Task Delete(int bookId)
    {
        await Http.DeleteAsync($"api/books/{bookId}");
        books = await Http.GetFromJsonAsync<List<Book>>("api/books");
    }
}

こんな感じになります。

Animation.gif

Update

バックエンド

BooksControllerUpdate メソッドを追加します。

BlazoreApp.Server/Controllers/BooksController.cs
using BlazorApp.Server.Data;
using BlazorApp.Shared.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace BlazorApp.Server.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BooksController : ControllerBase
    {
        private readonly AppDbContext context;

        public BooksController(AppDbContext context)
        {
            this.context = context;
        }

        [HttpGet]
        public async Task<ActionResult<List<Book>>> ListAsync()
        {
            var books = await context.Books.ToListAsync();
            return Ok(books);
        }

        [HttpGet("{id}")]
        public async Task<ActionResult<Book>> GetAsync(int id)
        {
            var book = await context.Books.SingleOrDefaultAsync(b => b.BookId.Equals(id));

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

            return Ok(book);
        }

        [HttpPost]
        public async Task<ActionResult<Book>> CreateAsync(Book book)
        {
            if (book.BookId != 0)
            {
                return BadRequest();
            }

            context.Books.Add(book);
            await context.SaveChangesAsync();

            return CreatedAtAction("Get", new { id = book.BookId }, book);
        }

        [HttpDelete("{id}")]
        public async Task<ActionResult> DeleteAsync(int id)
        {
            var book = await context.Books.SingleOrDefaultAsync(b => b.BookId.Equals(id));

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

            context.Books.Remove(book);
            await context.SaveChangesAsync();

            return NoContent();
        }

        [HttpPut]
        public async Task<ActionResult> Update(Book book)
        {
            if (await context.Books.AsNoTracking().SingleOrDefaultAsync(b => b.BookId == book.BookId) == null)
            {
                return NotFound();
            }

            context.Entry(book).State = EntityState.Modified;
            await context.SaveChangesAsync();
            return NoContent();
        }
    }
}

フロントエンド

BookEdit コンポーネントを追加します。

@using BlazorApp.Shared.Models
@using System.Text.Json
@inject HttpClient Http
@inject NotificationService Notice

@if (Book == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <Form Model="@Book" OnFinish="Update" LabelColSpan="4" WrapperColSpan="20">
        <FormItem Label="ID">
            @context.BookId
        </FormItem>
        <FormItem Label="Title">
            <Input @bind-Value="@context.Title" />
        </FormItem>
        <FormItem Label="Author">
            <Input @bind-Value="@context.Author" />
        </FormItem>
        <FormItem WrapperColSpan="24" Style="text-align: center">
            <Button Type="primary" HtmlType="submit">Update</Button>
        </FormItem>
    </Form>
}

@code {
    [Parameter]
    public Book? Book { get; set; }

    [Parameter]
    public EventCallback Callback { get; set; }

    private async Task Update()
    {
        await Http.PutAsJsonAsync("api/books", Book);

        await Callback.InvokeAsync();
        await Notice.Open(new NotificationConfig()
        {
            Message = "更新しました!"
        });
    }
}

ボタンをクリックすると BookEdit コンポーネントをモーダルに表示するように BookList コンポーネントを修正します。

@page "/booklist"
@using BlazorApp.Shared.Models
@inject HttpClient Http

@if (books == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <Button Type="primary" OnClick="OpenCreateModal" Block>New Book</Button>
    <Divider></Divider>

    <Table TItem="Book" DataSource="@books">
        <Column @bind-Field="@context.BookId" Title="ID">
            <Button Type="link" OnClick="() => OpenDetailModal(context.BookId)">@context.BookId</Button>
        </Column>
        <Column @bind-Field="@context.Title" />
        <Column @bind-Field="@context.Author" />
        <ActionColumn>
            <Button OnClick="() => OpenEditModal(context.BookId)">Edit</Button>
        </ActionColumn>
        <ActionColumn>
            <Popconfirm Title="本当に削除しますか?" OkText="はい" CancelText="いいえ" OnConfirm="() => Delete(context.BookId)">
                <Button Danger>Delete</Button>
            </Popconfirm>
        </ActionColumn>
    </Table>

    <Modal Title="Detail" Visible="@showDetailModal" OnCancel="@CloseModal" Footer="@null">
        <BookDetail Book="@book"></BookDetail>
    </Modal>

    <Modal Title="Create" Visible="@showCreateModal" OnCancel="@CloseModal" Footer="@null">
        <BookCreate Book="@book" Callback="@OnCallback"></BookCreate>
    </Modal>

    <Modal Title="Edit" Visible="@showEditModal" OnCancel="@CloseModal" Footer="@null">
        <BookEdit Book="@book" Callback="@OnCallback"></BookEdit>
    </Modal>
}

@code {
    private List<Book>? books;
    private Book? book;
    private bool showDetailModal = false;
    private bool showCreateModal = false;
    private bool showEditModal = false;

    protected override async Task OnInitializedAsync()
    {
        books = await Http.GetFromJsonAsync<List<Book>>("api/books");
    }

    public async Task OpenDetailModal(int id)
    {
        book = await Http.GetFromJsonAsync<Book>($"api/books/{id}");
        showDetailModal = true;
    }

    public void CloseModal()
    {
        showDetailModal = false;
        showCreateModal = false;
        showEditModal = false;
        book = null;
    }

    private void OpenCreateModal()
    {
        book = new(0, "", "");
        showCreateModal = true;
    }

    private async Task OnCallback()
    {
        books = await Http.GetFromJsonAsync<List<Book>>("api/books");
        this.CloseModal();
    }

    public async Task Delete(int bookId)
    {
        await Http.DeleteAsync($"api/books/{bookId}");
        books = await Http.GetFromJsonAsync<List<Book>>("api/books");
    }

    private async Task OpenEditModal(int id)
    {
        book = await Http.GetFromJsonAsync<Book>($"api/books/{id}");
        showEditModal = true;
    }
}

こんな感じになります。

Animation.gif

ソースコード

26
29
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
29