ひとりでのアプリ開発 - fineの備忘録 -

ひとりでアプリ開発をするなかで起こったことや学んだことを書き溜めていきます

Blazor - サンプルを確認する①

初めに

 Blazor WebAssembly アプリのサンプルを見て、プロジェクトの構成を学びます。

プロジェクトの作成

Blazor WebAssembly App の作成

 Blazor WebAssembly App を作成します。

 次の二つにチェックを入れて作成します。

 プロジェクトが作成されました。

 プロジェクトが「-.Client」「-.Server」「-.Shared」の3項目で構成されていることが分かります。
 

 そのまま、実行すると、サンプルとして含まれている「Home」「Counter」「Fetch data」を確認できます。

(Home)

(Counter)

(Fetch data)

 画面右上の About をクリックすると、Microsoft のドキュメントを開くことができます。

プロジェクトの構成

全体像

 プロジェクトが「-.Client」「-.Server」「-.Shared」の3項目で構成されています。

 それぞれの役割は次の通りです。

プロジェクト 説明
.Client ブラウザ上で実行されるクライアント側のプログラムを書く
.Server サーバー側で動作するプログラムを書く
Shared クライアントとサーバーの両方で共有されるプログラムを書く

Client プロジェクト


Pages フォルダ

 URL に対応した画面を表す .razor 拡張子をもつファイルを置いておくフォルダです。

 サンプルには、「Counter.razor」「Fetch.razor」「Index.razor」が含まれています。

〇 Index.razor

@page "/"

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

 Razor 構文と呼ばれる C# と HTML を組み合わせたようなコードで書かれます。

 "@page" の部分でURLを定義しています。Index.razor ではスラッシュのみのため、ホーム画面になるように定義されています。

 <PageTitle>はページタイトル、<h1>は見出しです。

 <SurveyPrompt Title> は子コンポーネントとして作成されているイベントを呼び出しています。

 SurbeyPrompt.razor というファイルがクライアントプロジェクトの Shared フォルダ内にあります。

〇 SurveyPrompt.razor

<div class="alert alert-secondary mt-4">
    <span class="oi oi-pencil me-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold link-dark" href="https://go.microsoft.com/fwlink/?linkid=2186157">brief survey</a>
    </span>
    and tell us what you think.
</div>

@code {
    // Demonstrates how a parent component can supply parameters
    [Parameter]
    public string? Title { get; set; }
}

 SurveyPrompt.razor に Title という処理が定義してあり、Index.razor ではそれを呼び出しています。

 子コンポーネントを使うことで、関数を定義することができると思ってよいでしょう。

〇Counter.razor

@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}

 @page "/counter" は、このページが "/counter" のURLにマップされることを示しています。つまり、WebアプリケーションのルートURLに "/counter" を追加すると、このページが表示されます。

<p role="status">Current count: @currentCount</p>

 これは、現在のカウントを表示するための段落を作成します。@currentCount は、コードの中で定義された currentCount 変数の値を表示するために使用されます。

 role(ロール)はHTML5で追加された新しい属性です。roleを使うことで、「この要素にはこういった役割がある」と明示的に示すことができます。

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

 "Click me" というテキストが表示されるボタンを作成します。@onclick="IncrementCount" は、ボタンがクリックされたときにIncrementCount メソッドが呼び出されることを示しています。

 @codeブロックは、C#コードを含むセクションです。ここでは、currentCount変数を定義し、初期値を0に設定しています。また、IncrementCountメソッドを定義しています。このメソッドは、呼び出されるたびにcurrentCountの値をインクリメントします。

〇FetchData.razor


@page "/fetchdata"
@using BlazorTodoTest.Shared
@inject HttpClient Http

<PageTitle>Weather forecast</PageTitle>

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
    }
}

 @using BlazorTodoTest.Shared は BlazorTodoTest.Shared の子である WeatherForecast.cs を使うために書かれています。WeatherForecast.cs では、Date、TemperatureC、TemperatureF、Summary が定義されているクラスがあります。

〇WeatherForecast.cs


namespace BlazorTodoTest.Shared
{
    public class WeatherForecast
    {
        public DateOnly Date { get; set; }

        public int TemperatureC { get; set; }

        public string? Summary { get; set; }

        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    }
}

 @inject HttpClient Http は、依存性注入(DI:Dependency Injection)と呼ばれています。依存関係の注入により、コンポーネントは HttpClient のインスタンスを自分自身で作成するのではなく、外部から提供されるインスタンスを使用します。これにより、このページで再度 HttpClient のインスタンスを作成せずに使用することができます。FetchData.razor 内では、@code の中で HttpClient が使われています。

 この HttpClient のインスタンスは、Program.cs で生成されています。

 builder.Services.AddScoped の部分で、HttpClient のインスタンスが生成されていることが分かります。

〇 Program.cs

using BlazorTodoTest.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) });

await builder.Build().RunAsync();

 依存性注入(DI)についての説明は参考にあるリンク先をご覧ください。

 FetchData.razor の

forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");

では、サーバー内にある WeatherForecatController.cs の Get 関数が呼び出されています。 サーバー側で動作する処理は「-.Server」内に書き、クライアント側から呼び出された形になっています。


Properties フォルダ

 launchSettings.json が入っています。launchSettings.json は、端的に言うと起動プロファイルです。デバッガーからプログラムを起動する際の各種設定をプロファイル単位で定義できます。

 どのプロファイルから起動するかはデバッグ実行ボタンから選択することができます。


Shared フォルダ

 UI コンポーネントを置いておくフォルダです。

〇 MainLayout.razor
 すべてのページに共通するレイアウトをここで書いています。

@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
        </div>

        <article class="content px-4">
            @Body
        </article>
    </main>
</div>

 @inherits はクラスの継承に使います。レイアウトを定義するクラスは「LayoutComponentBase」を継承する必要があります。

 サイドバーは NavMenu で記述するように指定してあります。

 top-row はヘッダーの About の部分を指定しています。

 「@Body」ディレクティブが、各ページ(各コンポーネント)固有のコンテンツが配置される部分になります。

〇 NavMenu.razor
 サイドバーについて、記述しています。

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

<div class="@NavMenuCssClass nav-scrollable" @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>
    </nav>
</div>

@code {
    private bool collapseNavMenu = true;

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

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

〇 SurveyPrompt.razor
 説明は index.razor の部分でしたため、割愛。

wwwroot フォルダ

 Web ルートは、次のような、パブリックで静的なリソース ファイルへの基本パスです。

_imports.razor

 各画面の .razor ファイルで毎回 @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 Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using BlazorTodoTest.Client
@using BlazorTodoTest.Client.Shared
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>

 ルーティングは、ウェブアプリケーションにおいて、特定のURLに対してどのコンテンツや機能を表示するかを決定する仕組みです。具体的には、ユーザーが特定のURLにアクセスしたときに、対応するコンテンツや画面を表示するための処理を実行します。

 上のコードでは、次のような処理をしています。

  • URLが見つかったら、MainLayout をデフォルトのレイアウトにして、該当ページに遷移する
  • URL が見つからなかったら "Sorry, there ..." と表示する

〇 Program.cs
 クライアント側の開始プログラムです。

using BlazorTodoTest.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) });

await builder.Build().RunAsync();

 WebAssemblyHostBuilder.CreateDefault(args); では、最も一般的な規則と設定を使用して、WebAssemblyHostBuilderのインスタンスを作成します。

 builder.RootComponents.Add("#app"); は、wwwroot フォルダにある index.html で id="app" と定義した部分が指定されます。

(index.html の抜粋)

<div id="app">
        <svg class="loading-progress">
            <circle r="40%" cx="50%" cy="50%" />
            <circle r="40%" cx="50%" cy="50%" />
        </svg>
        <div class="loading-progress-text"></div>
</div>

 builder.RootComponents.Add("head::after"); では、HeadOutlet コンポーネントが追加されています。これにより <title> タグや <head> タグのコンテンツをページから設定できます。

 builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); は依存性注入に関連する部分になります。説明は参考にあるリンクをご覧ください。

 await builder.Build().RunAsync(); で最後にビルド、実行しています。

次回へ

 記事が長くなってしまったので、Server プロジェクト、Shared プロジェクトについては別でまとめます。