入門者向け

【Blazor】プロジェクト全体の構成を詳しく解説する

project-structure

初めて Blazor のプロジェクトを作り、動かしたところまでやった人は多いと思います。

ですが実際にプログラムの中身を見てみると、全体的な構造や文法があまり理解できないんですよね。

公式ドキュメントには部分的な解説は載っていますが、「サンプルアプリがどう動いているのか」までは解説されていません。

そこで本記事では、サンプルアプリがどのように動いているのかをひと通り解説しました。

「全体的な構造を理解したい」という人におすすめな内容です。

まだプロジェクトを作成していない人は、【入門者向け】Blazor のはじめかた【環境構築から初回実行まで】を読んでから戻ってきてください。
ASP.NET Core でホストされている設定の前提です。

プロジェクトの構成は次のようになっています。

blazor-how-to-start

それでは、さっそく見ていきましょう。

Client プロジェクト

Client はクライアント側で動作するプログラムを書くプロジェクトですね。

詳しく見ていきましょう。

Pages フォルダ

URL に対応する画面を置いておくフォルダです。

デフォルトは次の3画面が用意されています。

  • Index.razor
  • Counter.razor
  • FetchData.razor

Index.razor

具体的にプログラムに中身を見てみましょう。

Client.Pages.Index.razor
@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

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

@page を指定することで、ページの具体的な URL を定義することができます。

スラッシュが定義されているので、ホーム画面として表示されるというわけです。

それより下は HTML を書くことで画面として表示されます。

ペンギンくん
ペンギンくん
SurveyPrompt ってタグは初めて聞いた。

SurveyPrompt という見慣れないタグがありますね。

これは何かというと、子コンポーネントとして作成されている UI 部品です。

実際にプログラムの中身を見てみましょう。

Client.Shared.SurveyPrompt.razor
<div class="alert alert-secondary mt-4" role="alert">
    <span class="oi oi-pencil mr-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold" href="https://go.microsoft.com/fwlink/?linkid=2137916">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; }
}

子コンポーネントの場合は、単体でページにはならないため、@page の宣言はありません。

同じような UI をいろんな画面でも使用したい場合に、何も考えなければコピペになってしまいますよね。

子コンポーネントを使用することで、プログラムで言うところの「メソッド化」をすることができ、コピペをしなくてもよくなります。

呼び出し元を「親コンポーネント」と呼び、子コンポーネントを呼び出すときにはパラメータを渡すことも可能です。

パラメータとして受け取りたいプロパティに対して [Parameter] のアトリビュートをつけることで、メソッドの引数の役割をつけることができます。

サンプルでは Title プロパティを受け取って、文字列を表示する仕組みになっているので、動的に表示する文字列を変えることができるというわけです。

Counter.razor

Index.razorでは @page の指定が「/」でしたが、今回は「/counter」になっていますね。

@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

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

@code {
    private int currentCount = 0;

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

p タグの中に@currentCount という表現があります。

これは、C# 側で定義した変数をタグに組み込み、動的に値を変えることができる仕組みです。

@code の中身には、C# のロジックを書きます。

C# の内部変数として currentCount が定義されていますね。

HTML のタグ内で参照するときは、@ をつければ C# の変数という意味になります。

また、ボタンタグに「@onclick=”IncrementCount”」という表現を見つけましたか?

これは、「ボタンをクリックしたときに IncrementCount のメソッドを呼びますよ」という意味です。

ボタンを押すことで変数がインクリメントされ、画面上の表示も1ずつ増えていくというわけですね。

FetchData.razor

FetchData.razorは、サーバー側からデータを受け取って画面に表示するサンプルになっています。

少し長いですが、落ち着いて見ていけば難しくないので安心してください。

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

<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");
    }

}

先ほど「@code で囲まれたところに C# の処理を書く」と説明しましたが、実は HTML 部分にも C# の処理を書くことができます。

@if@foreach の部分ですね。@をつけてあげることで、条件によってタグを表示したりしなかったり制御できます。

上の方を見ると @using の記述がありますが、これは C# の使い方と同じで参照の追加です。

その下にある @inject HttpClient Http は、依存性注入(DI:Dependency Injection)と呼ばれています。

ざっくり「インスタンスの使い回しができる宣言」と覚えておけばいいでしょう。

よく見ると Program.csAddScoped で追加されているので、見てみてください。

宣言した Http を使用して、サーバー側の WeatherForecastController の Get メソッドを呼んでいます。

Properties フォルダ

launchSettings.json はローカルでのみ使用されるもので、開発環境の構成を書いておくファイルですが、あまり触ることはないと思います。

Shared フォルダ

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

MainLayout,razor

@inherits LayoutComponentBase

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

    <div class="main">
        <div class="top-row px-4">
            <a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
        </div>

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

 

ナビゲーションメニューやヘッダ、フッタの情報は、どのページでも同じレイアウトになりますよね。

どのページでも同じなのに、各ページにプログラムを書いていては無駄が発生してしまいます。

そこで、あらかじめ全体的なレイアウトを決めておくことで、各ページで違うところだけ置き換えるのみで済む仕組みが用意されています。

LayoutComponentBase を継承することで、レイアウトのコンポーネントとして定義ができます。

@Body は、各ページのことを指しています(Index.razor など)。

NavMenu.razor

ナビゲーションメニューを定義している UI コンポーネントです。

<div class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href="">SampleApp</a>
    <button class="navbar-toggler" @onclick="ToggleNavMenu">
        <span class="navbar-toggler-icon"></span>
    </button>
</div>

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="counter">
                <span class="oi oi-plus" aria-hidden="true"></span> Counter
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="fetchdata">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
            </NavLink>
        </li>
    </ul>
</div>

@code {
    private bool collapseNavMenu = true;

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

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

NavLink コンポーネントは a タグのように動きますが、選択したメニューをハイライトしてくれたり、href は最低限のパスだけ指定するだけでよくなったりします。

wwwroot フォルダ

CSS や Javascript、画像など静的なファイルを置いておく場所です。

favicon.ico はブラウザで開いたときのアイコンになります。

index.html は、読み込む Javascript や CSS を書いておくファイルです。

参考:Web ルート

_imports.razor

各画面の .razor ファイルで毎回 @using を書かなくて済むように、まとめて参照を追加することができるファイルです。

よく使う参照は、_imports.razor に追加しておきましょう。

App.razor

ルーティングを設定するコンポーネントです。

<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

ざっくりと以下の動きになっています。

  • デフォルトのレイアウトは MainLayout を使用する
  • 指定した URL が存在すれば該当ページに遷移する
  • 指定した URL が存在しなければ「Sorry,…」を表示する

MainLayout については、Shared フォルダの説明を参照してください。

Program.cs

Client 側の開始プログラムです。

using System;
using System.Net.Http;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Text;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace SampleApp.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

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

            await builder.Build().RunAsync();
        }
    }
}

先ほど解説した HttpClient の依存性注入もここでやっていますね。

Server プロジェクト

Server はサーバー側で動作するプログラムを書くプロジェクトですね。

詳しく見ていきましょう。

Controllers フォルダ

Controllers フォルダには、クライアント側から API として呼び出す処理を書くクラスをまとめておきます。

サンプルでは、WeatherForecastController.cs が用意されていますね。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using SampleApp.Shared;

namespace SampleApp.Server.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

[Route("[controller]")] というのは、リクエスト URL をコントローラー名を基準に設定するという意味です。

WeatherForecastController が名前なので、クライアント側から WetherForecast で呼び出せば OK です。

この API は FetchData.razor から呼び出されています。

Pages フォルダ

Error.cshtml はアプリ側でハンドリングできなかったエラーが発生したときに表示されます。

Properties フォルダ

Client 側と同じ情報が書かれています。

appsettings.json

アプリの構成設定を書いておくファイルです。

Program.cs

サーバー側の開始プログラムです。

Startup.cs

こちらも開始時に読み込まれるプログラムで、お作法の処理がほとんどです。

認証を追加するときなどは、このファイルに処理を追加したりします。

Shared プロジェクト

Shared はクライアント側とサーバー側で共通して使用したいプログラムを入れておくプロジェクトです。

サンプルでは WeatherForecast.cs が用意されています。

中身を見るとエンティティとして使っているので、データ保持を目的として使っていますね。

あとは、ユーティリティ的な処理とかも置いておくと便利かもしれません。

ペンギンくん
ペンギンくん
ふー、たくさんあるから繰り返し読んで理解しよう。

最後に

Blazor のプロジェクトを新規作成したときの構成について解説しました。

かなり盛りだくさんだったので、全部を一気に覚えられなかったかもしれません。

私も最初はさっぱりでしたが、繰り返し処理を追っていくごとに少しずつ理解できるようになってきました。

この記事を読んで、少しずつ自分なりにカスタマイズもしてみましょう。

Blazor の書籍も好評発売中!
blazor-book

入門編から EC サイトを作る応用編まで、Blazor の本を3冊執筆しました。

私が1年以上かけて学習した内容をすべて詰め込んでいるので、さらにステップアップしたい方はぜひご覧ください。