BlazorでSPAするぞ!(1) - 正式版対応済

※最終更新日: 2020/5/24 正式リリース版に対応修正しました。

1. Blazorとは

公式Docsでは以下のように書かれています。

Blazor is a framework for building interactive client-side web UI with .NET

「Blazorは、.NETでインタラクティブなクライアントサイドWebユーザーインターフェイスを構築するためのフレームワークです。」ってことですね。
エンジニア的には、抽象的なモヤモヤ感のある文章ですね。
具体的に出来ることを簡単に言うと、今までJavaScript(+各種JSフレームワーク)でやってた事を、.NET Core(つまりC#)で作ることができるよ、ってものです。
JavaScript(or TypeScript) + React」とか「JavaScript(or TypeScript) + Vue」とかで作ってたようなSPAなアプリケーションをBlazorで作ることができます。
UI要素はHTML・CSSで構築し、ロジック部分をC#で記述するようなイメージになります。
メリットとしては「サーバーサイドもクライアントサイドもC#で開発できる」ってことです。
そもそもサーバーサイドにおいて非.NETなエンジニアにとっては、メリットとはならないものですが、.NETディベロッパーにとってはクライアントサイドもC#で書けるというのは生産性とかメンテナンス性とかでメリットがあります。
従来のWeb開発の React/Redux/TypeScript/Flow/Vue/Angular/Babel... の技術たちと比べるとキワ物感がありますが(Blazorは.NETに絞られる技術になるので)、ASP.NET Core+BlazorでのSPA開発は、かなり生産性高そうという印象を持っています(まだPreviewだし、実開発してない段階での感想ですが)。

1.1. WebAssembly

C#コードがクライアントサイドで動作するというキーワードは、過去にもいくつかありましたね。。。

2020年5月現在においては、オープンな技術開発的には使えない技術達ですね(一部のtoBでは生き残ってると思いますが・・・)。

で、Blazorがクライアントサイド(ブラウザ)でC#コードの動作を実現している仕組みですが、「WebAssembly」になります。
WebAssemblyは C/C++・Rust・Go言語 なんかで作れるようになっていますが、BlazorによってC#でもWebAssemblyが作れるようになりました。
厳密には、Blazorの.NET Coreアセンブリを.NET WASMでWebAssemblyにビルドして実行するような形になります。

図にすると以下のような感じ。

f:id:daigo-knowlbo:20200524125611p:plain

BlazorとDOMとの間に矢印がありますが、Blazorの中でプログラムによって画面要素に変更があるとそれがDOMに伝えられて画面の更新が行われます。
Reactの仮想DOMと同様に、Blazorでは「render tree」と呼ばれるUI要素構造をメモリに展開し、変更の必要がある部分のみDOMに反映させる仕組みを持っています。

1.2. Blazor WebAssembly(Client-Side Blazor) と Blazor Server(Server-Side Blazor)

Blazorは上述のようにクライアントサイドでC#が動作する技術(Blazor WebAssembly)ですが、これ以外に同じ実装をサーバーサイドで動作させるBlazor Serverというものがあります。
Blazorコンポーネントをサーバーサイドで実行する仕組みのものになります。クライアントサイドはプレーンなHTML+JSでWebAssemblyは実行されません。
実行イメージは以下のような感じ。

f:id:daigo-knowlbo:20190501214537p:plain

ボタンのクリックなど「Blazor的に影響のある事象が発生する」と、Signal Rでサーバにリクエストが行われ、サーバサイド(ASP.NET Core)でBlazorが動作し、結果をクライアントに送り返す。結果に応じたDOMの更新がクライアントで行われます(Web Formのポストバックみたいな処理が裏側で自動的に行われます)。

※ この投稿では特別に記述がない限り Blazor WebAssembly を前提とした説明を行います。

2. 開発環境構築

開発環境はWindowsでもMacでも行けますが、ここではWindowsを前提とします。

2.1. .net Core 3インストール

以下から.NET Core 3.1 SDKをダウンロードしてインストールします。

https://dotnet.microsoft.com/download/dotnet-core/3.1

コマンドプロンプトでバージョンを確認。

C:>dotnet --version 3.1.300

※2020/5/24現在の最新は 3.1.300

### 2.2. Blazorテンプレートのインストール コマンドプロンプトで以下のコマンドを実行します。
(dotnet newコマンドに、Blazorのプロジェクトテンプレートがインストールされます)

# 正式リリース版で不要になりました。
dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.0.0-preview6.19307.2

2.3. C# for Visual Studio Codeのインストール(Visual Studio Code利用の場合)

Visual Studio Codeを使う場合は、「C# for Visual Studio Code」をインストールしておきます。

https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp

### 2.4. Visual Studio Extensionインストール(Visual Studio 2019利用の場合) Visual Studio 2019を使う場合は、「BlazorのExtension」をインストールしておきます。
(Visual Studio 2019に、Blazorのプロジェクトテンプレートがインストールされます)

https://marketplace.visualstudio.com/items?itemName=aspnet.blazor

※本記事では Visual Studio Code を開発に利用します。(VS2019だと.razorのファイルの追加がUIから出来ない等Extensionが十分にPreview版に追い付いていない感があったので)

3. Blazorプロジェクト作成

では早速Blazorプロジェクトを作成します。

3.1. dotnet コマンド / 4つのプロジェクトテンプレート

VS Codeベースで進めるのでdotnetコマンドを使っていきます。

まずプロジェクトテンプレートの確認を。
dotnet new」でテンプレート一覧を確認すると以下のようにBlazor関連のテンプレートが4種類表示されます。

C:\>dotnet new
使用法: new [オプション]
...省略

Templates                                         Short Name            Language          Tags
-------------------------------------------------------------------------------------------------------------------------------
...省略
Blazor Server App                                 blazorserver             [C#]              Web/Blazor
Blazor WebAssembly App                            blazorwasm               [C#]              Web/Blazor/WebAssembly
...省略

それぞれ以下の通りです。

(1) Blazor Server App

Blazor ServerのASP.NET Coreプロジェクトテンプレートです。

dotnet new blazorserver -o BlazorServerApp1

作成されるファイル達↓↓↓

f:id:daigo-knowlbo:20200524124904p:plain

(2) Blazor WebAssembly App

Blazor WebAssemblyのプロジェクトテンプレートです。

dotnet new blazorwasm -o BlazorWasmApp1

作成されるファイル達↓↓↓

f:id:daigo-knowlbo:20200524124942p:plain

(3) Blazor WebAssembly App with hosted app

blazorwasmテンプレートに --hosted オプションを追加すると、Blazor WebAssembly + ASP.NET Core WebAPI構成のソリューションが作成されます。
Blazor WebAssemblyの通信先WebAPIサービスとしてのASP.NET Coreプロジェクトもセットで作成されるイメージです。

dotnet new blazorwasm --hosted -o BlazorWasmAppWithHosted1

作成されるファイル達↓↓↓
f:id:daigo-knowlbo:20200524125006p:plain

4. Hello Blazor WebAssembly

では、純粋にBlazorを試すために「Blazor WebAssembly App(blazorwasm)」でプロジェクトを作成します。

ますプロジェクトを作成。hello_blazorという名前にします。

dotnet new blazorwasm -o hello_blazor

4.1. Visual Studio Codeで開く

VS Codeの「File - Open Folder」メニューで 作成した hello_blazor フォルダを開きます。

f:id:daigo-knowlbo:20200524125049p:plain

4.2. ビルド + 実行する

ある程度の動作する実装がテンプレートで出力されているので、ひとまずそのままビルドします。
コマンドプロンプトでプロジェクトフォルダに移動し、「dotnet build」コマンドを実行します。

daigo@ardentred MINGW64 /c/workspace/hello_blazor
$ dotnet build
.NET Core 向け Microsoft (R) Build Engine バージョン 16.6.0+5ff7b0c9e
Copyright (C) Microsoft Corporation.All rights reserved.

  復元対象のプロジェクトを決定しています...
  復元対象のすべてのプロジェクトは最新です。
  hello_blazor -> C:\workspace\hello_blazor\bin\Debug\netstandard2.1\hello_blazor.dll
  hello_blazor (Blazor output) -> C:\workspace\hello_blazor\bin\Debug\netstandard2.1\wwwroot

ビルドに成功しました。
    0 個の警告
    0 エラー

経過時間 00:00:03.30

obj / binフォルダが作成され、その下にアセンブリ(.dll)が出力されました。

では以下のように dotnet run コマンドを実行します。

daigo@ardentred MINGW64 /c/workspace/hello_blazor
$ dotnet run
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\workspace\hello_blazor

Blazor自体はClient sideのWebAssembly(の上で動く.NETアセンブリ)ですが、http://localhost:5000 / https://localhost:5001 としてhello_blazorをホストするHttpサーバが立ち上がります。
ブラウザで http://localhost:5000 にアクセスすると、テンプレートで出力したサンプル実装が起動します。

f:id:daigo-knowlbo:20200524125116p:plain

Hello World以上の立派な実装がなされています。

ちなみにこのページを表示した時のHttpRequestの状況ですが、以下のようになっています。

f:id:daigo-knowlbo:20200524125133p:plain

よく見ると「blazor.webassembly.js」とか「dotnet.wasm」とか「hello_blazor.dll」とか「Microsoft.AspNetCore.Components.WebAssembly.dll」とか・・・.NETが動こうとしている事が見て取れると思います。

それから、左のサイドメニューから「+Counter」を選択すると、ボタンクリックでカウントアップが行われる画面に移動します。
「Click me」ボタンをクリックすると、Current Countが1ずつカウントアップしますが、この時サーバへのリクエストが飛んでいないことも確認できます。
(当然ながらクライアントサイドでC#コードが実行されているので、このようなロジック処理でサーバへのラウンドトリップは発生しない、という事です)

f:id:daigo-knowlbo:20200524125154p:plain

4.3. Hello Blazorに着手!

何もしないで http://localhost:5000 にアクセスした画面がすでに「Hello, World!」になっています。
これを「Hello Blazor」に改造していこうと思います。
Blazorが動作するベースの仕組みは色々ありますが、、、とりあえず このページは「Pages/index.razor」で実装されています。

# Pages/Index.razor #

@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

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

実行画面と見比べると 大体想像できる感じ ですよね(HTMLタグが書かれていて、ASP.NET CoreのRazorを知っていれば @xx なRazor構文が書かれているな・・と)。
画面上にヘッダーやサイドメニューが付いてるのはレイアウト設定が効いているからです(ASP.NET Coreの _Layout.cshtml とかと同じような仕組みがBlazorにもあり、それが適用されています )。
先頭の「@page "/"」は後(後続の投稿)で詳細を説明しますが、ルーティング設定です。"/"なのでルートとしてこのIndex.razorがルーティング適用されています。

(1) プロパティ変数の利用

以下のような画面に改造したいと思います。

f:id:daigo-knowlbo:20200524125221p:plain

HTMLをべた書きすれば実現できるのですが、それではあまり意味がないので、無理やりC#なコードを使っていきます。

# Pages/Index.razor(Hello Blazor版 その1) #

@page "/"

<h1>@Message</h1>

@code {
  private string Message = "Hello Blazor";
}

「@Message」は、C#で定義した変数(プロパティ)をレンダリングします。ASP.NETのRazorと完全に一緒です。
「@code」は、このコンポーネント(Index.razor)のコードブロックを表します。コードブロックには、C#で.NETコードを記述することができます。
string型のMessage変数を定義し、"Hello Blazor"で初期化しているのは見れば分かるでしょう。

こんな感じでクライアントサイド実装をC#で行うことができます。

(2) イベントの使用

では、次に以下のような画面に改造します。
「get date and time」ボタンを用意し、クリックされたらすぐ上にテキストで現在日時を表示します。

f:id:daigo-knowlbo:20200524125242p:plain

# Pages/index.razor(Hello Blazor版 その2) #

@page "/"

<h1>@Message</h1>
<br />
CurrentDateTime:@CurrentDateTime
<br />
<input type="button" @onclick="GetDateTime" value="get date and time" />

@code {
  private string Message = "Hello Blazor";
  private string CurrentDateTime = "???";
  
  private void GetDateTime() {
    CurrentDateTime = DateTime.Now.ToString("yyyy/MM/dd H:m:s");
  }
}
@CurrentDateTime

CurrentDateTime変数を用意し(初期値は ??? )、「CurrentDateTime:@CurrentDateTime」として画面に表示しています。

ボタン

「<input type="button" @onclick="GetDateTime" value="get date and timee" />」でボタンを配置しています。
「@onclick="GetDateTime"」がポイントで、onclickはHTML/JavaScriptにおけるinputタグのイベントと同様です。
これに対して GetDateTime() メソッドをバインディングしています(onclickに対してJavaScriptのFunctionをバインドするのと同じ感覚ですね)。
GetDateTime()では DateTime.Now.ToString() という.NETのC#コードにより CurrentDateTime 変数を変更しています。
CurrentDateTimeの変更を受けBlazor Runtimeは、対象箇所の再レンダリングを行います。

良い感じにプロパティとイベントをC#で実装し、HTML要素に反映することが出来ましたね。

※ 「@onclick="GetDateTime"」の@GetDateTime部分は将来的に「@プレフィックス」を不要とする方針のようです。 ← 正式版出で不要になりました。

4.4. CodeBehindを使う

上述のIndex.razorは、.razorファイルに UI要素 も コード も混ざった状態になってしまいました。
よくある話ですが、Blazorでも UIとコード は分離可能です。
「UI(razor定義)」と「コード(C#ページロジック)」を分離する方法は2つあります。
1つは「razor定義のpartialクラスを実装する方法」、もう1つは「定義したページクラスをrazorで継承する方法」です。

(1) razor定義のpartialクラスを実装する方法

以下がrazor定義です。
@codeセクションを定義せず、UI定義のみを行っています。

# Pages/Index.razor (razor定義のpartialクラスを実装する方法) #

@page "/"

<h1>@Message</h1>
<br />
CurrentDateTime:@CurrentDateTime
<br />
<input type="button" @onclick="GetDateTime" value="get date and time" />

次に、ページロジッククラスを定義するIndex.csファイルを用意します。
定義するクラス名はrazor定義のファイル名「Index」と同様にします。また、partialクラスとして定義することが重要です。
razor定義側で要求される仕様に合わせ、Message / CurrentDateTimeプロパティ、GetDateTime()メソッドを実装します。

# Pages/Index.cs (razor定義のpartialクラスを実装する方法) #

using System;

namespace hello_blazor.Pages
{
  public partial class Index
  {
    private string Message { get; set; } = "Hello Blazor";

    private string CurrentDateTime { get; set; }= "???";

    private void GetDateTime() {
      CurrentDateTime = DateTime.Now.ToString("yyyy/MM/dd H:m:s");
    }
  }
}
何故、クラス名をIndexにして、partialクラスで定義し、privateメンバーを定義したのか?

「razor定義のpartialクラスを実装する方法」は上記に説明した通りですが、「何故~」について少し深堀したいと思います。
答えは、コーディングしたhello_blazorを「dotnet build」により生成した「hello_blazor.dll」を見ることで明確になります。
hello_blazor.dllを ILSpy で逆コンパイルした結果が以下です。

f:id:daigo-knowlbo:20200524125308p:plain

// hello_blazor.Pages.Index
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
using System;

[Route("/")]
public class Index : ComponentBase
{
    private string Message
    {
        get;
        set;
    } = "Hello Blazor";


    private string CurrentDateTime
    {
        get;
        set;
    } = "???";


    private void GetDateTime()
    {
        CurrentDateTime = DateTime.Now.ToString("yyyy/MM/dd H:m:s");
    }

    protected override void BuildRenderTree(RenderTreeBuilder __builder)
    {
        __builder.OpenElement(0, "h1");
        __builder.AddContent(1, Message);
        __builder.CloseElement();
        __builder.AddMarkupContent(2, "\r\n<br>\r\nCurrentDateTime:");
        __builder.AddContent(3, CurrentDateTime);
        __builder.AddMarkupContent(4, "\r\n<br>\r\n");
        __builder.OpenElement(5, "input");
        __builder.AddAttribute(6, "type", "button");
        __builder.AddAttribute(7, "onclick", EventCallback.Factory.Create<MouseEventArgs>((object)this, (Action)GetDateTime));
        __builder.AddAttribute(8, "value", "get date and time");
        __builder.CloseElement();
    }
}

Index.razor / Index.razor.cs がコンパイルされることで、「Microsoft.AspNetCore.Components.ComponentBase」クラスを継承した「Index」クラスとなりました(名前空間は hello_blazor.Pages)。

まず、前提として .razor 定義は「Microsoft.AspNetCore.Components.ComponentBaseを継承したクラス(名前空間・クラス名は、プロジェクト・フォルダ名・razorファイル名から決定)」にコンパイルされます。そして、HTMLタグ や @プレフィックス 等による定義は「BuildRenderTree()」メソッド内でC#コードによる要素構築に変換されます。
つまり、Index.razor.cs で上記名前空間・クラス名に合わせたクラスを partial で定義すれば、razorから自動生成されるクラスのパーシャルクラス定義とすることができます。
これが「class partial Index { }」がIndex.razorの分離コード定義となる仕組みです。
そして、「Message / CurrentDateTime / GetDateTime()」が private のスコープ定義で事足りた理由も同じところにあります。Index.razorから自動生成されたIndexクラスと、Index.razor.csで定義したIndexクラスとは、partialによりコード上分離した位置に定義しただけであり、コンパイル後の実体は同一だからです。

(2) 定義したページクラスをrazorで継承する方法

以下がrazor定義です。

# Pages/Index.razor (定義したページクラスをrazorで継承する方法) #

@page "/"
@inherits IndexBase

<h1>@Message</h1>
<br />
CurrentDateTime:@CurrentDateTime
<br />
<input type="button" @onclick="GetDateTime" value="get date and time" />

「@inherits IndexBase」により、このrazor定義はIndexBaseクラスを継承することを明示しています。

次に、ページロジッククラスを定義するIndexBase.csファイルを用意します。
定義するクラス名は、razorファイル定義内の@inheritsでも指定した通り「IndexBase」です。
ページクラス(厳密にはBlazorのコンポーネントクラス)は「Microsoft.AspNetCore.Components.ComponentBase」を継承して作成します。
razor定義側で要求される仕様に合わせ、Message / CurrentDateTimeプロパティ、GetDateTime()メソッドを実装します。

# Pages/Index.cs (定義したページクラスをrazorで継承する方法) #

using System;
using Microsoft.AspNetCore.Components;

namespace hello_blazor.Pages
{
  public class IndexBase : ComponentBase
  {
    protected string Message { get; set; } = "Hello Blazor";

    protected string CurrentDateTime { get; set; }= "???";

    protected void GetDateTime() {
      CurrentDateTime = DateTime.Now.ToString("yyyy/MM/dd H:m:s");
    }
  }
}
@inherits、partialでなくなったページクラス定義、プロパティ・メソッドはprotectedになった

partialクラスによるUIとロジックの分離実装からいくつかの要素に変更が加わっています。
再度、コンパイルにより得られた「hello_blazor.dll」を見ると根柢の仕組みが見えてきます。
先ほどと同様に ILSpy を使ってみましょう。

f:id:daigo-knowlbo:20200524125326p:plain

// hello_blazor.Pages.Index
using hello_blazor.Pages;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
using System;

[Route("/")]
public class Index : IndexBase
{
    protected override void BuildRenderTree(RenderTreeBuilder __builder)
    {
        __builder.OpenElement(0, "h1");
        __builder.AddContent(1, base.Message);
        __builder.CloseElement();
        __builder.AddMarkupContent(2, "\r\n<br>\r\nCurrentDateTime:");
        __builder.AddContent(3, base.CurrentDateTime);
        __builder.AddMarkupContent(4, "\r\n<br>\r\n");
        __builder.OpenElement(5, "input");
        __builder.AddAttribute(6, "type", "button");
        __builder.AddAttribute(7, "onclick", EventCallback.Factory.Create<MouseEventArgs>((object)this, (Action)base.GetDateTime));
        __builder.AddAttribute(8, "value", "get date and time");
        __builder.CloseElement();
    }
}

// hello_blazor.Pages.IndexBase
using Microsoft.AspNetCore.Components;
using System;

public class IndexBase : ComponentBase
{
    protected string Message
    {
        get;
        set;
    } = "Hello Blazor";


    protected string CurrentDateTime
    {
        get;
        set;
    } = "???";


    protected void GetDateTime()
    {
        CurrentDateTime = DateTime.Now.ToString("yyyy/MM/dd H:m:s");
    }
}

IndexクラスとIndexBaseクラスが別々に定義されています。そして、IndexはIndexBaseを継承しています。
これは実装した通りですが、IndexBaseクラスはpartialではなくComponentBaseクラスを継承した独立定義したクラスとして実装しました(IndexBase.cs)。
そして Index.razor では、「@inherits IndexBase」と明示的にIndexBaseクラスを継承する定義を行いました。

「.razorによりUI定義を行った結果生成されたIndexクラス」と「UIロジック実装として用意したIndexBaseクラス」は継承関係にあることから、「Message / CurrentDateTimeプロパティ、GetDateTime()メソッド」は privateスコープでは不足、「protected」以上のスコープが必要となります。

アーキテクチャ的には MVVM や Flux/Redux で実装ような選択肢もあるので、コードを @code{} に書くとか コードビハインドに書くとか がベストプラクティスどうかは、プロジェクトごとの選択になります。

5. まとめ

という感じで、Blazorではじめの一歩が踏み出せました!

次は ryuichi111std.hatenablog.com