Blazor のPath Base(pathbase)について

1. はじめに

Blazorのプロジェクトを作成すると、テンプレート選択が「Blazor(client)でもBlazor(ASP.NET core hosted)でも」、dotnet run によるデフォルトの実行構成は http://localhost:5000 でアプリをホストするようになります。
開発サーバーなのでhostはlocalhostでportも5000ですが、今回注目する意味としては「ホスト上のルート(/)」での動作が前提となっています。

本投稿は、Path Base設定により「http://hogehoge/MyApp」などのサブディレクトリ配下での動作を行うことができる点の説明になります。

※ ここではClient side Blazorを対象としています。

2. 開発サーバとサブディレクト

まず、開発環境においてサブディレクトリでアプリをホストして起動する方法は dotnet run に「--pathbase」オプションを付与します。

dotnet run --pathbase=/MyApp

上記コマンドを実行すると、http://localhost:5000/MyApp でアプリをホストして開発サーバが実行されます。

http://localhost:5000 にアクセス】

http://localhost:5000 にアクセスすると /MyApp でリクエストをハンドルするように構成されているよ、とレスポンスしてきます。

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

http://localhost:5000/MyApp にアクセス】

http://localhost:5000/MyApp にアクセスすると、以下のように Loading... で止まります。

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

なぜ Loading... で止まっているかというと・・・Chrome DevToolsでNetworkを確認してみます。

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

404 Not foundが出ていますね。
http://localhost:5000/_framework/blazor.webassembly.js」など、ルート(/)配下を取りに行っています(アプリは/MyAppサブディレクトリで動作させているので、当然404します)。

3. index.htmlの<base>タグ設定

Blazorが動作する仕組みとしては、以下です。

①ブラウザがHTTP GETでindex.htmlを読み込む
②(index.htmlの記述きっかけで)Blazorを動作させるためのjs(blazor.webassembly.js)が読み込まれる(その他CSS等も)
③mono wasmが読み込まれる
④Blazorアセンブリ(dll)が読み込まれる

で、index.htmlを確認すると以下のようにbaseタグがルート(/)指定で書かれています(<base href="/" />)。

# wwwroot/index.html #

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>clientapp</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/site.css" rel="stylesheet" />
</head>
<body>
    <app>Loading...</app>

    <script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

という事で素直に /MyApp/ に書き換えてあげます(後ろの / も忘れずに)。

<base href="/MyApp/" />

ちなみに baseタグ は、Blazor固有ではなく一般的なHTML仕様です(文書の基底URL指定)。

developer.mozilla.org

あらためて dotnet run すると、すべての関連ファイルが http://localhost:5000/MyApp/ 配下から読み込まれて正しく動作します。

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

3. まとめ

ということで、 以下がまとめです。

  • 開発サーバは dotnet run --pathbase=xxx オプションを指定するとサブディレクトリでアプリをホストできる。
  • プロジェクトテンプレートが自動生成したindex.html は、<base> タグで読み込むjs/cssのパス調整が行われるようになっている。

Blazor でトースター表示する(sotsera.blazor.toaster)

1. はじめに

Blazorでトースター メッセージを表示する sotsera.blazor.toaster を使ってみました。

github.com

2. サンプル実装

ではサクッと。

2.1. プロジェクト作成

プロジェクトを作成します。
プロジェクト名は use-sotsera-toaster としました。

dotnet new blazor -o use-sotsera-toaster

use-sotsera-toasterディレクトリに移動し、「Sotsera.Blazor.Toaster」パッケージを追加します。
2019.5.16 現在の最新バージョン 0.9.0-preview-3 を入れました。

cd use-sotsera-toaster
dotnet add package Sotsera.Blazor.Toaster --version 0.9.0-preview-3

www.nuget.org

2.2. サービス登録

StartupクラスのConfigureServices()でサービス登録を行います。

# Startup.cs #

using Microsoft.AspNetCore.Components.Builder;
using Microsoft.Extensions.DependencyInjection;

using Sotsera.Blazor.Toaster.Core.Models;

namespace use_sotsera_toaster
{
  public class Startup
  {
    public void ConfigureServices(IServiceCollection services)
    {
      services.AddToaster(config =>
      {
        config.PositionClass = Defaults.Classes.Position.TopRight;
        config.MaximumOpacity = 80;
        config.VisibleStateDuration = 10000;
        config.HideTransitionDuration = 3000;
        config.NewestOnTop = false;
        config.ShowCloseIcon = true;
        config.ShowProgressBar = false;
        config.PreventDuplicates = true;
      });
    }

    public void Configure(IComponentsApplicationBuilder app)
    {
      app.AddComponent<App>("app");
    }
  }
}

【using Sotsera.Blazor.Toaster.Core.Models;】

config設定で「Defaults.Classes.Position.TopRight」を使用するために、このusingを追加しています。
AddToaster()メソッドはSotsera.Blazor.Toasterアセンブリ内でMicrosoft.Extensions.DependencyInjection名前空間に実装されています。

【services.AddToaster()】

トーストサービスを追加します。
config設定を行うことでトースト表示の振る舞いをカスタマイズできます。
サンプルで設定したconfigは以下の通りです。

  • PositionClass
    トーストを表示する画面上の位置を設定します。サンプルでは右上を設定。

  • MaximumOpacity
    初期表示時のトーストの透過率を設定します。
    100%が透過なしです。サンプルではトースト初期表示時に80%の透過率で表示されます。

f:id:daigo-knowlbo:20190516022202p:plain
※よく見ると右上の About 文字とかが透過してるのが分かります。
※トーストは消える際に徐々に薄くなります。

  • VisibleStateDuration
    トーストが表示されてから消え始めるまでの時間をmsで指定します。

  • HideTransitionDuration
    トーストが表示からVisibleStateDuration(ms)経過した後から、徐々に薄くなって完全に消えるまでの時間をmsで指定します。

  • NewestOnTop
    複数のトーストが同時に表示される場合、新しいトーストを上に表示する場合は true 、下に表示する場合は false を指定します。
    trueの場合:
    f:id:daigo-knowlbo:20190516022833p:plain
    falseの場合:
    f:id:daigo-knowlbo:20190516022854p:plain

  • ShowCloseIcon
    トースト右上の閉じるアイコンの表示/非表示設定を行います。

  • ShowProgressBar
    トーストが消えるまでの時間を示すプログレスバーの表示/非表示設定を行います。

  • PreventDuplicates
    同一のメッセージがすでにトースト表示されている場合に、別のトーストとして追加表示するかどうかを設定します。
    falseの場合:
    f:id:daigo-knowlbo:20190516023228p:plain

trueの場合:(同じメッセージのToaster.Info()呼び出しが何回行われてもトーストは1つに制御される)
f:id:daigo-knowlbo:20190516023249p:plain

2.3. ページでトースト表示を実装

Index.razorページにボタンを用意し、ボタンがクリックされたらトーストが表示されるようにします。
トーストには、スタイルの異なる Info / Success / Warning / Error が用意されているので、それぞれを表示するボタンを用意することとします。

# Pages/Index.razor #

@page "/"
@using Sotsera.Blazor.Toaster
@inject Sotsera.Blazor.Toaster.IToaster Toaster
<ToastContainer />

<input type="button" value="Show info" onclick="@ShowInfo" />
<input type="button" value="Show info" onclick="@ShowSuccess" />
<input type="button" value="Show info" onclick="@ShowWarning" />
<input type="button" value="Show info" onclick="@ShowError" />

@functions {
  private void ShowInfo() {
    Toaster.Info("info message");
  }

  private void ShowSuccess() {
    Toaster.Success("Success message");
  }

  private void ShowWarning() {
    Toaster.Warning("Warning message");
  }
  
  private void ShowError() {
    Toaster.Error("Error message");
  }
}

【@using Sotsera.Blazor.Toaster】

ページ内で IToaster を使用するためにusing宣言を行います。

【@inject Sotsera.Blazor.Toaster.IToaster Toaster】

IToasterインターフェイスオブジェクトをインジェクトし Toaster 変数に格納します。

【<ToastContainer />】

ToastContainerオブジェクトをページ内に配置しておきます。
インジェクトした Toaster オブジェクトの「トースト表示メソッド呼び出し時」のUIレンダリングに利用されます。

【Toaster.Info() / Toaster.Success() / Toaster.Warning() / Toaster.Error() 】

各ボタンのonclickイベントハンドラ内でToasterオブジェクトのトースト表示メソッドを呼び出します。

2.4. 実行

以下のコマンドで実行します。

dotnet run

http://localhost:5000 にアクセスし、4つのボタンをクリックした画面が以下です。

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

3. まとめ

という事で、簡単にトースト表示実装を行えました。

今回作成したサンプルは↓です。

github.com

Blazor Extensions Logging を使ってログ出力する

1. はじめに

Blazorでのロギングを試したいと思います。

まず本投稿で動作確認した各種環境は以下の通りです。

  • Windows 10 x64
  • .NET Core 3.0.100-preview5-011568
  • Blazor Extensions Logging 0.1.11

最もシンプルなロギングは標準機能「Console.WriteLine()」を使う方法です。

Console.WriteLine("Blazorからログ出力!!");

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

ただし、今回ロギングに使うのはコレ!!↓↓↓。

github.com

Blazor Extensions Loggingを使用すると Console.WriteLine() よりもリッチなロギング処理が可能になります。

2. サンプルで試す

では、早速サンプルで試してみましょう。

2.1. プロジェクトの作成

まず、プロジェクトを作成します。
テンプレートはシンプルに「Client Side Blazor」、プロジェクト名は「use-logging-extension」とました。

dotnet new blazor -o use-logging-extension

2.2. Nugetでパッケージを追加

コマンドプロンプトでuse-logging-extensionフォルダに移動します。
続けて、以下のコマンドで Blazor Extensions Logging パッケージをプロジェクトに追加します。
(バージョンは2019.5.14時点の最新である 0.1.11 )

dotnet add package Blazor.Extensions.Logging --version 0.1.11

NuGet Gallery | Blazor.Extensions.Logging 0.1.11

2.3. 初期化

Startupクラスの ConfigureServices() メソッドでLoggingの初期化(サービスへの追加)を行います。
以下のように「2つのusing」と「services.AddLogging()呼び出し」を追加します。

# Startup.cs #
...省略...

using Microsoft.Extensions.Logging;
using Blazor.Extensions.Logging;

namespace use_logging_extension {
  public class Startup {

    public void ConfigureServices(IServiceCollection services) {
      services.AddLogging(builder => builder
        .AddBrowserConsole()
        .SetMinimumLevel(LogLevel.Trace)
      );    
    }
    ...省略...
  }
}

2.4. テキストログの出力

シンプルなテキストのログ出力を行います。
Pages/Index.razor に実装を行います。
「ログ出力(テキスト)」というキャプションのボタンを配置し、クリックされたら「called WriteTextLog()」というテキストログを出力します。

# Pages/Index.razor #

@page "/"
@using Microsoft.Extensions.Logging
@inject ILogger<Index> logger

<input type="button" 
       onclick="@WriteTextLog"
       value="ログ出力(テキスト)" />

@functions {
  private void WriteTextLog() {
    logger.LogDebug("called WriteTextLog()");
  }
}

【@using Microsoft.Extensions.Logging】

Blazor Extensions Loggingは、.NET CoreのILoggerの仕組みの上にインプリメントされています。
ILoggerを使用するためにまず Microsoft.Extensions.Loggingをusingします。

【@inject ILogger logger】

ILoggerを、DIでこのページの変数 logger にインジェクトします。

【logger.LogDebug()】

LogDebug()メソッドでログ出力を行います。
LogDebug()の他に「LogTrace() / LogInformation() / LogWarning() / LogError() / LogCritical()」メソッドが用意されています。

実行してみましょう。

dotnet run

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

「ログ出力(テキスト)」ボタンをクリックすると、以下のようにChrome DevToolsのConsoleにログが出力されました。

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

2.5. カスタムオブジェクトログの出力

次にカスタムオブジェクトのログ出力を行います。
以下のように Models/Employee.cs クラスを作成します。

# Models/Employee.cs #

namespace use_logging_extension.Models {
  public class Employee {
    public int ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
  }
}

先程作成した Pages/Index.razor を改良します。
「ログ出力(カスタムオブジェクト)」というキャプションのボタンを追加で配置し、クリックされたら「Employeeオブジェクト」をログ出力します。

# Pages/Index.razor #

@page "/"
@using use_logging_extension.Models
@using Microsoft.Extensions.Logging
@using Blazor.Extensions.Logging
@inject ILogger<Index> logger

<input type="button"
       onclick="@WriteTextLog"
       value="ログ出力(テキスト)" />
<input type="button"
       onclick="@WriteCustomObjectLog"
       value="ログ出力(カスタムオブジェクト)" />


@functions {
  private void WriteTextLog() {
    logger.LogDebug("called WriteTextLog()");
  }

  private void WriteCustomObjectLog() {
    Employee employee = new Employee() {
      ID = 1000, FirstName = "ryuichi", LastName = "daigo" };
    logger.LogDebug<Employee>(employee);
  }
}

【@using use_logging_extension.Models】

Index.razorにおいて、Employeeオブジェクトを使用するためにusingを追加します。

【@using Blazor.Extensions.Logging】

logger.LogDebug<T>()を利用しますが、このメソッド定義は「Blazor.Extensions.Logging.BrowserConsoleLoggerExtensions拡張メソッド」で実装されているため、Blazor.Extensions.Loggingへのusingが必要になります。

【logger.LogDebug<Employee>(employee);】

logger.LogDebug<Employee>()メソッドでカスタムオブジェクトのログ出力を行うことができます。

実行してみましょう。

dotnet run

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

「ログ出力(カスタムオブジェクト)」ボタンをクリックすると、以下のようにChrome DevToolsのConsoleにログが出力されました。
リッチな階層構造でのログ出力が行われています。

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

2.5. ログ出力レベル

ロギングライブラリを利用したことがあれば、大体想像がつくと思いますが、ログ出力レベルの調整が可能です。
Startup#ConfigureServices() メソッドでの初期化処理を以下のように書き換えれば Error以上のログのみ出力されるようになります。

# Startup.cs #

services.AddLogging(builder => builder
    .AddBrowserConsole()
    .SetMinimumLevel(LogLevel.Error)
  );    

3. まとめ

Blazor Extensions Loggingは、.NET CoreからのILoggerの作法の上に実装されているライブラリなので、非常にスムーズに感覚的に利用することができました。
開発レベルでは手軽に便利に使えるのではないかと思います。
あと、運用段階でクライアントログをサーバーにの集積したい、等の要望がある場合は、もちろん別途実装ですね。

また、今回作成したサンプルコードは以下に置いてあります。

github.com

Blazor [tips]: bindとonchangeの併用は不可

1. はじめに

Blazorを調査しているときに、データバインディングについて理解していない段階で はまった ことを思い出したのでTipsとして書いておきます。
「inputタグに"bind属性"と"onchange属性"を両方つけると、(おそらく)想定した動きをしないよねー」という話です。

2. サンプル

具体的には以下のコードです。

# Pages/index.razor #

@page "/"

<input type="text"
  bind="@inputValue"
  onchange="@(e => inputValueChanged(e.Value.ToString()) )"/>
<br />
→ inputValue: <div>@inputValue</div>
→ changedValue: <div>@changedValue</div>

@functions {
  private string inputValue { get; set; } = "";
  private string changedValue { get; set; } = "";

  void inputValueChanged(string newValue) {
    changedValue = newValue;
  }
}

実行画面

以下が実行画面です。

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

テキストボックスに「はろー」と入力して、フォーカスをテキストボックスから外します。
そうすると以下の画面のようになります。

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

実装コード的には以下を想定したのですが、、、

inputValue: はろー
changedValue: はろー

以下のようになりました。

inputValue: はろー
changedValue: 

結論としては「onchangeが効いていない」という事です。
記事タイトルの通り「bindとonchangeの併用は不可」って事ですね。

onchange="@(e => inputValueChanged(e.Value.ToString()) )"

3. なんで bindとonchangeは併用 できない?

ということで、先程のページ(index.razor)をビルドしたアセンブリ(dll)をILSpyで覗いてみます。
(補足:ここではindex.razorはhello_bind_appというBlazorプロジェクトに実装しました)

// hello_bind_app.Pages.Index
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;

protected override void BuildRenderTree(RenderTreeBuilder builder)
{
  builder.OpenElement(0, "input");
  builder.AddAttribute(1, "type", "text");
  builder.AddAttribute(2, "onchange", EventCallback.Factory.Create(this, delegate(UIChangeEventArgs e)
  {
    inputValueChanged(e.Value.ToString());
  }));
  builder.AddAttribute(3, "value", BindMethods.GetValue(inputValue));
  builder.AddAttribute(4, "onchange", EventCallback.Factory.CreateBinder(this, delegate(string __value)
  {
    inputValue = __value;
  }, inputValue));
  builder.CloseElement();
  builder.AddMarkupContent(5, "\r\n<br>\r\n→ inputValue: ");
  builder.OpenElement(6, "div");
  builder.AddContent(7, inputValue);
  builder.CloseElement();
  builder.AddMarkupContent(8, "\r\n→ changedValue: ");
  builder.OpenElement(9, "div");
  builder.AddContent(10, changedValue);
  builder.CloseElement();
}

Blazorのページレンダリング(RenderTreeの構築)の仕組みとして、ページクラスの BuildRenderTree() メソッドがその役割を担います。
ここに .razor で定義した「HTML / CSS / インラインで記述したrazorコード」が、コードとして展開されています。

onchangeイベントに対するハンドリング処理は以下のように、良い感じに構築されています。

  builder.AddAttribute(2, "onchange", EventCallback.Factory.Create(this, delegate(UIChangeEventArgs e)
  {
    inputValueChanged(e.Value.ToString());
  }));

が、その直後に同じinputタグに対して再度 onchange 属性がaddAttribute()されています。
これは「bind="@inputValue"」によるtwo-wayデータバインディングを構築する実装に該当します。

  builder.AddAttribute(3, "value", BindMethods.GetValue(inputValue));
  builder.AddAttribute(4, "onchange", EventCallback.Factory.CreateBinder(this, delegate(string __value)
  {
    inputValue = __value;
  }, inputValue));

同じinputタグの onchange属性 に対して2回AddAttribute()が行われ、結果として後勝ちとなり「onchange="@(e => inputValueChanged(e.Value.ToString()) )」は無かったものとなってしまいます。

という事で、「なんで bindとonchangeは併用 できない?」の根拠がすっきりしました。

valueとonchangeの併用はおk

bindではなく、valueによるone-wayバインディングではonchangeがハンドルされないので、以下の onchange は有効に働きます。

# Pages/index.razor #

...省略...
<input type="text"
  value="@inputValue"
  onchange="@(e => inputValueChanged(e.Value.ToString()) )"/>
...省略...

ビルド後のアセンブリ実装は以下。

// hello_bind_app.Pages.Index
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;

protected override void BuildRenderTree(RenderTreeBuilder builder)
{
  builder.OpenElement(0, "input");
  builder.AddAttribute(1, "type", "text");
  builder.AddAttribute(2, "value", inputValue);
  builder.AddAttribute(3, "onchange", EventCallback.Factory.Create(this, delegate(UIChangeEventArgs e)
  {
    inputValueChanged(e.Value.ToString());
  }));
  builder.CloseElement();
  builder.AddMarkupContent(4, "\r\n<br>\r\n→ inputValue: ");
  builder.OpenElement(5, "div");
  builder.AddContent(6, inputValue);
  builder.CloseElement();
  builder.AddMarkupContent(7, "\r\n→ changedValue: ");
  builder.OpenElement(8, "div");
  builder.AddContent(9, changedValue);
  builder.CloseElement();
}

まあ、このサンプルではbindによる動作が落ちることになるので、これが正解実装というわけではありません。
以下のような画面になる。changedValueには反映するが、two-wayではなくなったのでinputValueには反映しない。

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

3. ということは bindとoninputの併用はおk

bindはonchangeに展開されるので、bindとoninputの併用は問題ありません。

# Pages/index.razor #

...省略...
<input type="text"
  bind="@inputValue"
  oninput="@(e => inputValueChanged(e.Value.ToString()) )"/>
...省略...

oninputは入力ごとのイベントが発生するので、値の反映のタイミングは異なりますが、以下の画面のようにbindとoninputは両方が有効に動作していることが確認できます。

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

4. まとめ

ということで、
「bindはコンパイル後に onchange を含む実装に展開されるから、Developer側のonchange定義が無効になっちゃうよ」
そんなTipsでした。

Blazor-Fluxorで簡易Todoアプリを作る

1. はじめに

Microsoft Build 2019 盛り上がってますね、たぶん(僕は非MS系会社勤務なので分からぬ)。
.NET / Azureのたくさんのサービスの発表・GA等々行われていますが、ここでは今回もPreview版である(Client Side)Blazorについて取り上げます。
今回は Blazor-Fluxor というライブラリを使ってみようと思います。

開発環境

筆者の開発環境は以下の通り。

OS: Windows 10 x64
.net core version: 3.0.100-preview5-011568
Blazor-Fluxor: 0.22.0-pre
コマンドプロンプトVS Code

本投稿で作成したサンプル

本投稿で作成したサンプルは以下 です。

github.com

2. Blazor-Fluxor とは

Blazor-Fluxorとは、以下のgithubで公開されているOSSライブラリです。

github.com

トップの一文を引用すると以下の通りです。

A low-boilerplate Flux/Redux state library for Blazor

つまり、Blazorで Flux/Reduxな状態管理 を行うためのライブラリです。

公式サンプルが非常に親切なので、以下の「01-CounterSample」「02-WeatherForecastSample」あたりを、説明を読みながら自分で実装してみると分かりやすいと思います。

blazor-fluxor/samples at master · mrpmorris/blazor-fluxor · GitHub

また、「05-FlightFinder」は、より実践的なサンプルになっています。

ただし、Flux/Reduxの基本だけは別途押さえてから取り掛かる方がスムーズかと思います。
Blazor-Fluxorの解説では、Flux/Reduxの思想的な事には触れていないので(概要を知っている前提)。

3. 簡易Todoアプリを作ろう

公式サンプル「02-WeatherForecastSample」と「05-FlightFinder」の中間位の難易度の「簡易Todoアプリ」を作りたいと思います。
完成画面は以下の通りです。

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

機能としては以下とします。

  • クライアントはBlazorアプリ
  • Todo一覧取得/Todo登録は、ASP.NET CoreのWeb APIで実装
  • Todoを追加することができる(Blazorアプリで入力してWebAPI呼び出し)
  • エラー処理は簡易

3.1. プロジェクトを作成

dotnet new コマンドでプロジェクトを作成します。

dotnet new blazorhosted -o blazor-fluxor-todo-app

プロジェクト名は「blazor-fluxor-todo-app」です。
テンプレート:blazorhostedは、「Blazor (ASP.NET Core hosted)」で以下の3プロジェクト構成のファイルが出力されます。

  • blazor-fluxor-todo-app.Client
    Blazorクライアントアプリプロジェクト
  • blazor-fluxor-todo-app.Server
    ASP.NET Coreサーバープロジェクト(WebAPIを提供)
  • blazor-fluxor-todo-app.Shared
    共通プロジェクト。モデルクラスとか置くと「blazor-fluxor-todo-app.Client」「blazor-fluxor-todo-app.Server」の両方から使える。

3.2. 余分なコードを削除

テンプレートで自動生成されるコードはたくさんありますが、今回の実装でいらないコードを削除しておきます。

以下のファイルを削除します。

  • blazor-fluxor-todo-app.Client/Pages/Counter.razor
  • blazor-fluxor-todo-app.Client/Pages/FetchData.razor
  • blazor-fluxor-todo-app.Server/Controllers/SampleDataController.cs
  • blazor-fluxor-todo-app.Shared/WeatherForecast.cs

blazor-fluxor-todo-app.Client/Shared/NavMenu.razor からCounter/FetchDataへのメニューリンクを削除します。

# blazor-fluxor-todo-app.Client/Shared/NavMenu.razor #

<div class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href="">blazor-fluxor-todo-app</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>
    </ul>
</div>

@functions {
    bool collapseNavMenu = true;

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

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

3.3. Web API を実装

まず、Blazorクライアントから呼び出すWebAPIを実装します。

① TodoItemモデルクラスを追加

Todo項目を表す TodoItem モデルクラスを追加します。
Client / Serverの両方から利用するので blazor-fluxor-todo-app.Shared プロジェクトに TodoItem.cs を追加します。

# blazor-fluxor-todo-app.Shared/TodoItem.cs #

namespace blazor_fluxor_todo_app.Shared {
  public enum Priority
  {
    High,
    Normal,
    Low
  }

  public class TodoItem {
    public int ID { get; set; }
    public Priority Priority { get; set; }
    public string Title { get; set; }
  }
}

ID / Title / Priorityの3つのプロパティを持ちます。Priorityは列挙体とします。
シンプルな.NETのPOCOクラスです。

② TodoItemControllerクラスを追加

WebAPIを実装します。
以下の2つの機能を実装します。

  • Todo一覧を取得する
  • Todoを追加する

ASP.NET Coreプロジェクトである blazor-fluxor-todo-app.Server のControllersフォルダ配下に TodoItemController.cs を追加します。

# blazor-fluxor-todo-app.Server/Controllers/TodoItemController.cs #

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

using blazor_fluxor_todo_app.Shared;

namespace blazor_fluxor_todo_app.Server.Controllers
{
  [Route("api/[controller]")]
  public class TodoItemController : Controller
  {
    private static List<TodoItem> _TodoItems = new List<TodoItem>();

    static TodoItemController()
    {
      _TodoItems.Add(new TodoItem() { ID = 1, Title = "Blazorを調べる", Priority = Priority.High } );
      _TodoItems.Add(new TodoItem() { ID = 2, Title = "ASP.NET Coreで遊ぶ", Priority = Priority.Normal } );
      _TodoItems.Add(new TodoItem() { ID = 3, Title = "flutterでもふもふする", Priority = Priority.Normal } );
      _TodoItems.Add(new TodoItem() { ID = 4, Title = ".NET Framework 3を葬る", Priority = Priority.Low } );
      _TodoItems.Add(new TodoItem() { ID = 5, Title = "Durable Functionsする", Priority = Priority.Normal } );
    }

    [HttpGet("")]
    public IEnumerable<TodoItem> AllTodoItems()
    {
      return _TodoItems;
    }

    [HttpPost]
    public TodoItem AddTodoItem([FromBody] TodoItem todoItem)
    {
      todoItem.ID = _TodoItems.Count + 1;
      _TodoItems.Add(todoItem);
      return todoItem;
    }
  }
}

TodoItemのリストは、「static List _TodoItems」としてTodoItemControllerのstatic変数として保持しています。DBなどの処理を省くためにオンメモリで簡易的に保持する仕組みにしています。
TodoItemControllerのstaticコンストラクタで、初期値として5つのTodoItemを登録しています。
一覧取得(HttpGet) および Todo追加(HttpPost) は、そのままのASP.NET Core WebAPIの実装ですね。

3.4. Blazor-Fluxorパッケージ参照をnugetで追加

※これ以降は Blazorクライアント(blazor-fluxor-todo-app.Client)への修正作業になります。

コマンドプロンプトでBlazor-FluxorパッケージへのNuget参照を追加します。
Blazor-Fluxorパッケージを利用するのはクライアント(Blazor)プロジェクトなので、blazor-fluxor-todo-app.Clientフォルダに移動して「dotnet add package」コマンドを実行します。

cd blazor-fluxor-todo-app.Client
dotnet add package Blazor.Fluxor --version 0.22.0-pre

※ 2019/5/9時点のBlazor-Fluxorの最新は 0.22.0-pre
https://www.nuget.org/packages/Blazor.Fluxor/0.22.0-pre

3.5. Blazor-Fluxorサービスを追加

BlazorクライアントでBlazor-Fluxorサービスを利用する為に、Startupクラスでサービスを追加します。

blazor-fluxor-todo-app.Client/Startup.csに対して以下の2つの修正を行います。

  • using Blazor.Fluxor; を追加
  • Startup#ConfigureServices()に対してservices.AddFluxor()処理を追加
# blazor-fluxor-todo-app.Client/Startup.cs #

using Microsoft.AspNetCore.Components.Builder;
using Microsoft.Extensions.DependencyInjection;

using Blazor.Fluxor;

namespace blazor_fluxor_todo_app.Client
{
  public class Startup
  {
    public void ConfigureServices(IServiceCollection services)
    {
      services.AddFluxor(options => options
        .UseDependencyInjection(typeof(Startup).Assembly)
      );
    }

    public void Configure(IComponentsApplicationBuilder app)
    {
      app.AddComponent<App>("app");
    }
  }
}

3.6. Storeを初期化

Storeの初期化を行います。
blazor-fluxor-todo-app.Client/Shared/MainLayout.razorに対して以下の2つの修正を行います。

  • @inject Blazor.Fluxor.IStore Store を追加
  • @Store.Initialize(); を追加
# blazor-fluxor-todo-app.Client/Shared/MainLayout.razor #

@inherits LayoutComponentBase
@inject Blazor.Fluxor.IStore Store

@Store.Initialize();

<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>

3.7. TodoStateを追加

Storeに保持するStateを作成します。
ここでは TodoState とします。
blazor-fluxor-todo-app.Client/Store フォルダを作成し、TodoState.csファイルを追加します。

# blazor-fluxor-todo-app.Client/Store/TodoState.cs #

using System;
using System.Collections.Generic;
using System.Linq;

using blazor_fluxor_todo_app.Shared;

namespace blazor_fluxor_todo_app.Client.Store {
  public class TodoState {
    public TodoItem[] TodoItems { get; private set; }

    public string ErrorMessage { get; private set; }

    public TodoState() {
      this.TodoItems = Array.Empty<TodoItem>();
      this.ErrorMessage = "";
    }

    public TodoState(IEnumerable<TodoItem> todoItems, string errorMessage) {
      this.TodoItems = todoItems?.ToArray() ?? Array.Empty<TodoItem>();
      this.ErrorMessage = errorMessage;
    }
  }
}

TodoStateクラスはアプリケーションで「保持したい状態」を表すクラスです。
そのため、このアプリケーションで保持したい2つのプロパティを実装しています。
1つは「TodoItems」、画面に表示するTodoアイテム一覧を表します。サーバーのWebAPI経由で取得してBlazorクライアントに保持することを想定します。
もう1つは「ErrorMessage」、これはネットワークエラーが発生した際に表示するエラーメッセージを保持することを想定します。
それ以外には、初期化のためのコンストラクタを2つ実装しています。

3.8. TodoFeatureを追加

Stateとセットにする形でFeatureを実装します。
Storeフォルダに TodoFeature.cs を追加します。

# blazor-fluxor-todo-app.Client/Store/TodoFeature.cs #

using Blazor.Fluxor;

namespace blazor_fluxor_todo_app.Client.Store
{
    public class TodoFeature : Feature<TodoState>
    {
        public override string GetName() => "Todo";
        protected override TodoState GetInitialState() => new TodoState();
    }
}

必ず、Blazor.Fluxor.Feature<T>クラスを継承します。
TはStoreに保持するStateクラスを設定します。
また、GetName()メソッドをオーバーライドします。Stateの名前を任意の名称で返却します。
もう1つ、GetInitialState()メソッドをオーバーライドします。Stateの初期値を返却します(先程定義したTodoStateのデフォルトコンストラクタでのインスタンスオブジェクトを返却しています)。

3.9. Todo一覧取得機能の追加

Todo一覧取得処理機能を実装します。
Reduxテイストでの実装を行います。
つまり Actionクラス と Reducreクラス を実装します。また、Httpリクエストのような副作用は Effectクラス として実装します。

【FetchTodoItemsActionクラス】

FetchTodoItemsActionクラスを実装します。

# blazor-fluxor-todo-app.Client/Store/FetchTodoItemsAction.cs #

using Blazor.Fluxor;

namespace blazor_fluxor_todo_app.Client.Store
{
    public class FetchTodoItemsAction : IAction
    {
    }
}

Actionは「Blazor.Fluxor.IActionインターフェイス」を実装します。
IActionインターフェイスは、プロパティ定義・メソッド定義が1つもないマーカーインターフェイスであるため、必要なければ何も実装する必要はありません。
アクションに対してパラメータが伴う場合には適時プロパティ定義を追加します。
ここでは、一覧取得に際しパラメータは不要なので何もプロパティの追加は行っていません。

【FetchTodoItemsEffectクラス】

Redux的には「Actionに対してReducerを用意します」が、FetchTodoItemsActionに関しては、そのアクションに対応してHttpリクエスト(WebAPI呼び出し)が必要になります。
つまり副作用処理が必要になります。
副作用はBlazor-Fluxorでは Effectクラス として実装を行います。

# blazor-fluxor-todo-app.Client/Store/FetchTodoItemsEffect.cs #

using Blazor.Fluxor;
using blazor_fluxor_todo_app.Shared;
using Microsoft.AspNetCore.Components;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace blazor_fluxor_todo_app.Client.Store
{
    public class FetchTodoItemsEffect : Effect<FetchTodoItemsAction>
    {
        private readonly HttpClient HttpClient;

        public FetchTodoItemsEffect(HttpClient httpClient)
        {
            HttpClient = httpClient;
        }

        protected async override Task HandleAsync(FetchTodoItemsAction action, IDispatcher dispatcher)
        {
            TodoItem[] todoItems = Array.Empty<TodoItem>();
            try
            {
                todoItems = await HttpClient.GetJsonAsync<TodoItem[]>("api/todoitem");
            }
            catch
            {
                dispatcher.Dispatch(new NetworkErrorAction("通信エラーが発生しました"));
                return;
            }
            var completeAction = new FetchTodoItemsCompleteAction(todoItems);
            dispatcher.Dispatch(completeAction);
        }
    }
}
Blazor.Fluxor.Effect<T>

Effectは「Blazor.Fluxor.Effectクラス」を継承します。
Effect<T>の「T」は、このEffectを実行するきっかけのactionクラスを定義します。
つまり FetchTodoItemsAction がDispatchされたときに、この FetchTodoItemsエフェクトクラスはキックされます。

HttpClientをインジェクト

Httpリクエスト処理を行うため、コンストラクタで「HttpClient」をDIによりインジェクトしてもらっています。
HttpClientはBlazorにおいてはデフォルトサービスなので、何も事前宣言不要でサービスクラスに対するコンストラクタインジェクションを利用可能です。

HandleAsyncをオーバーライド

「Task HandleAsync(FetchTodoItemsAction action, IDispatcher dispatcher)」をオーバーライドすることで、このEffectの動作を実装することができます。
Todo一覧を取得するWebAPI「api/todoitem」を呼び出し、TodoItemリストを取得しています。
取得が成功したら「FetchTodoItemsCompleteActionクラス」をDispatchしています(FetchTodoItemsCompleteActionクラスは次に説明)。
何らかのエラーが発生したら「NetworkErrorActionクラス」をDispatchしています。

【FetchTodoItemsCompleteActionクラス】

FetchTodoItemsActionのDispatchをきっかけにキックされた、FetchTodoItemsEffectがTodoリストの取得に成功した場合、FetchTodoItemsCompleteActionクラスを生成してDispatchしていました。
その FetchTodoItemsCompleteAction の定義です。

# blazor-fluxor-todo-app.Client/Store/FetchTodoItemsCompleteAction.cs #

using Blazor.Fluxor;
using blazor_fluxor_todo_app.Shared;

namespace blazor_fluxor_todo_app.Client.Store
{
    public class FetchTodoItemsCompleteAction : IAction
    {
        public readonly TodoItem[] TodoItems;

        public FetchTodoItemsCompleteAction(TodoItem[] todoItems)
        {
            this.TodoItems = todoItems;
        }
    }
}

actionなので IAction を実装します。
このアクションはTodoリストの取得完了を表すものなので、パラメータとして「TodoItems」が実装されます。

【FetchTodoItemsCompleteReducerクラス】

FetchTodoItemsCompleteActionに対応するReducerクラスを実装します。
前述のFetchTodoItemsActionとは異なり、FetchTodoItemsCompleteActionは副作用を伴わないので、素直に対応するReducerであるFetchTodoItemsCompleteReducerを定義し、これによりStateの更新を実装します。

# blazor-fluxor-todo-app.Client/Store/FetchTodoItemsCompleteReducer.cs #

using Blazor.Fluxor;

namespace blazor_fluxor_todo_app.Client.Store
{
    public class FetchTodoItemsCompleteReducer : Reducer<TodoState, FetchTodoItemsCompleteAction>
    {
        public override TodoState Reduce(TodoState state, FetchTodoItemsCompleteAction action)
        {
            return new TodoState(
             todoItems: action.TodoItems,
             errorMessage: ""
        );
        }
    }
}
Reducer<TState, TAction>を継承

Reducerは「Blazor.Fluxor.Reducer<TState, TAction>クラス」を継承します。
Reducer<TState, TAction>の「TState」は、このReducerが処理するStateクラスを定義します。「TAction」はこのReducerが処理するActionクラスを定義します。
つまり FetchTodoItemsCompleteReducerは、FetchTodoItemsCompleteAction の影響による状態変更の影響を TodoState に対して行うReducerクラスである、という事になります。
Reduce()メソッドをオーバーライドすることで、状態の更新を実装します。引数には「現在の状態 state」と「発生したアクション action」が渡されてきます。
第2引数 FetchTodoItemsCompleteAction action は、つまり FetchTodoItemsEffect による WebAPI呼び出しの結果得られたTodoリストが格納されています。

※ NetworkErrorAction / NetworkErrorReducerは省略。。。(Github上のコード見てm( )m)  

【画面表示と初期化 index.razor】

Todoリストを取得する裏側の処理について説明・実装してきましたが、画面表示について移ります。
index.razor画面にTodoリスト画面を実装します。
実装したいことは以下の2つです。

  • Storeに保存された TodoState を画面に一覧表示する(優先度 高は赤色表示)
  • 画面の初期化時に FetchTodoItemsAction アクションをDispatchして、WebAPIからTodoリストを取得する処理をキックする
# blazor-fluxor-todo-app.Client/Pages/index.razor #

@page "/"

@using Blazor.Fluxor
@using blazor_fluxor_todo_app.Client.Store
@using blazor_fluxor_todo_app.Shared

@inject IDispatcher Dispatcher
@inject IState<TodoState> TodoState

@TodoState.Value.ErrorMessage

<div class="card">
  <div class="card-header">Your Todo List</div>
  <div class="card-body">
    <table class="table">
      <thead>
        <tr>
          <th>ID</th>
          <th>Title</th>
          <th>Priority</th>
        </tr>
      </thead>
      @foreach(var todoItem in TodoState.Value.TodoItems) {
        string trClass = todoItem.Priority == Priority.High ? "table-danger" : "";
        
        <tr class="@trClass">
          <td>@todoItem.ID</td>
          <td>@todoItem.Title</td>
          <td>@todoItem.Priority</td>
        </tr>
      }
    </table>
  </div>
</div>

@functions {
    protected override void OnInit()
    {
        base.OnInit();
        
        TodoState.Subscribe(this);
        
        Dispatcher.Dispatch(new FetchTodoItemsAction());
    }
}
using と inject

Blazor.Fluxorおよびプロジェクトで定義したStore・Sharedへのusingを定義します。

Razorレンダリング定義

table形式でTodoリストを一覧表示する実装をRazor記述で行います。
データは「TodoState.Value.TodoItems」といった形式で、「【State名】.Value」の形式で参照可能です。

OnInit()

@functionsブロックのOnInit()は画面の初期化時に呼び出されます。
ここで「Dispatcher.Dispatch(new FetchTodoItemsAction());」とすることで、Todoリスト取得処理がキックされます。
「FetchTodoItemsAction → FetchTodoItemsEffect → FetchTodoItemsCompleteAction → FetchTodoItemsCompleteReducer → TodoState更新 → 画面に反映」

# TodoState.Subscribe(this);

これ、結構重要。
Httpリクエストのような、Blazor側が状態の変更タイミングを認識できない処理では、状態変更に伴う画面の更新が自動的に行われません。
そのため、StateのSubscribe()メソッドにより明示的に画面を登録しておく。
もしくは、.razorが「Blazor.Fluxor.Components.FluxorComponent」を継承する必要があります。

# blazor-fluxor-todo-app.Client/Pages/index.razor(FluxorComponentを継承する場合) #

@page "/"
@inherits Blazor.Fluxor.Components.FluxorComponent

@using Blazor.Fluxor
@using blazor_fluxor_todo_app.Client.Store
@using blazor_fluxor_todo_app.Shared

...省略...

3.10. Todo追加機能の追加

Todoを追加する機能の追加を行います。
Action / Effect / Reducerの追加を行います。
基本的に、前述のTodo一覧取得と同じ流れになるので説明はかなり端折りますm(_ _ )m。

【index.razor】

入力フォームを用意するので index.razor は以下のようになります。

# blazor-fluxor-todo-app.Client/Pages/index.razor #

@page "/"

@using Blazor.Fluxor
@using blazor_fluxor_todo_app.Client.Store
@using blazor_fluxor_todo_app.Shared

@inject IDispatcher Dispatcher
@inject IState<TodoState> TodoState

@TodoState.Value.ErrorMessage

<div class="card">
  <div class="card-header">Your Todo List</div>
  <div class="card-body">
    <table class="table">
      <thead>
        <tr>
          <th>ID</th>
          <th>Title</th>
          <th>Priority</th>
        </tr>
      </thead>
      @foreach(var todoItem in TodoState.Value.TodoItems) {
        string trClass = todoItem.Priority == Priority.High ? "table-danger" : "";
        
        <tr class="@trClass">
          <td>@todoItem.ID</td>
          <td>@todoItem.Title</td>
          <td>@todoItem.Priority</td>
        </tr>
      }
    </table>
  </div>
</div>

<hr />

<div class="card" style="width: 20rem;">
  <div class="card-header">Add new Todo</div>
  <div class="card-body">
    <div class="form-group">
      <label for="title">Title:</label>
      <input type="text" id="title" class="form-control" bind="@NewTodo" />
    </div>
    <div class="form-group">
      <label for="passwd1">Priority:</label>
      <select bind="@Priority">
          <option value=@Priority.High>@Priority.High.ToString()</option>
          <option value=@Priority.Normal>@Priority.Normal.ToString()</option>
          <option value=@Priority.Low>@Priority.Low.ToString()</option>
      </select>
    </div>
    <div class="form-group">
      <input type="button" class="btn btn-primary" onclick="@AddTodo" value="add todo" />
    </div>
  </div>
</div>

@functions {
    private string NewTodo { get; set; } = "";
    private Priority Priority { get; set; } = Priority.Normal;

    private void AddTodo()
    {
        var action = new AddTodoItemAction(this.NewTodo, this.Priority);
        Dispatcher.Dispatch(action);
    }

    protected override void OnInit()
    {
        base.OnInit();
        
        TodoState.Subscribe(this);
        
        Dispatcher.Dispatch(new FetchTodoItemsAction());
    }
}

NewTodo / Priorityプロパティを @functionsブロックに追加し、それぞれUI要素に対してbindしています。
ボタンのonclickに対しては AddTodo() がバインドされています。
AddTodo()メソッドでは、画面からの入力値(bindによるtwo-wayバインディングでプロパティに値反映)を使ってAddTodoItemActionアクションをDispatchしています。

【AddTodoItemActionアクション】

AddTodoItemActionはTodoの内容及び優先度パラメータを持つためプロパティが追加されています。

# blazor-fluxor-todo-app.Client/Store/AddTodoItemAction.cs #

using Blazor.Fluxor;
using blazor_fluxor_todo_app.Shared;

namespace blazor_fluxor_todo_app.Client.Store
{
  public class AddTodoItemAction : IAction
  {
    public readonly string Title;
    public readonly Priority Priority = Priority.Normal;

    public AddTodoItemAction() 
    {
    }
    
    public AddTodoItemAction(string title, Priority priority)
    {
      this.Title = title;
      this.Priority = priority;
    }
  }
}
【AddTodoItemCompleteActionアクション】

AddTodoItemActionはHttpリクエスト(WebAPI)副作用を伴うアクションであるため、その副作用(Effect)経由でDispatchされるアクションがAddTodoItemCompleteActionとなります。

# blazor-fluxor-todo-app.Client/Store/AddTodoItemCompleteAction.cs #

using Blazor.Fluxor;
using blazor_fluxor_todo_app.Shared;

namespace blazor_fluxor_todo_app.Client.Store
{
  public class AddTodoItemCompleteAction : IAction
  {
    public readonly TodoItem TodoItem;

    public AddTodoItemCompleteAction(TodoItem todoItem)
    {
      this.TodoItem = todoItem;
    }
  }
}
【AddTodoItemCompleteReducerリデューサ】

AddTodoItemCompleteActionアクションから状態を更新するReducerクラス実装です。

# blazor-fluxor-todo-app.Client/Store/AddTodoItemCompleteReducer.cs #

using System.Collections.Generic;
using Blazor.Fluxor;
using blazor_fluxor_todo_app.Shared;

namespace blazor_fluxor_todo_app.Client.Store
{
  public class AddTodoItemCompleteReducer : Reducer<TodoState, AddTodoItemCompleteAction>
  {
    public override TodoState Reduce(TodoState state, AddTodoItemCompleteAction action)
    {
      List<TodoItem> newTodoItems = new List<TodoItem>();
      foreach(var todoItem in state.TodoItems)
      {
        newTodoItems.Add(new TodoItem() { ID = todoItem.ID, Title = todoItem.Title, Priority = todoItem.Priority } );
      }
      newTodoItems.Add(action.TodoItem);

      return new TodoState(
        todoItems: newTodoItems,
        errorMessage: ""
        );
    }
  }
}
【AddTodoItemEffectエフェクト】

AddTodoItemActionアクション発生時に実行される副作用(Effect)の実装になります。
api/todoitemに対するHTTP POST処理を実行して、Todoの追加を行います。

# blazor-fluxor-todo-app.Client/Store/AddTodoItemEffect.cs #

using Blazor.Fluxor;
using blazor_fluxor_todo_app.Shared;
using Microsoft.AspNetCore.Components;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace blazor_fluxor_todo_app.Client.Store
{
  public class AddTodoItemEffect : Effect<AddTodoItemAction>
  {
    private readonly HttpClient HttpClient;

    public AddTodoItemEffect(HttpClient httpClient)
    {
      HttpClient = httpClient;
    }

    protected async override Task HandleAsync(AddTodoItemAction action, IDispatcher dispatcher)
    {
      TodoItem todoItem = new TodoItem() { Title = action.Title, Priority = action.Priority };
      try
      {
        todoItem = await HttpClient.PostJsonAsync<TodoItem>("api/todoitem", todoItem);
      }
      catch
      {
        dispatcher.Dispatch(new NetworkErrorAction("通信エラーが発生しました"));
        return;
      }
      var completeAction = new AddTodoItemCompleteAction(todoItem);
      dispatcher.Dispatch(completeAction);
    }
  }
}

実装が終わったら、blazor-fluxor-todo-app.Server フォルダで dotnet build / dotnet run で実行できます。

cd blazor-fluxor-todo-app.Server
dotnet build
dotnet run

実行できたら http://localhost:5000 にブラウザでアクセスしてみましょう。

4. Redux DevTools を使う

Blazor-Fluxorでは Redux DevTools を利用することが可能です。
Startup#ConfigureServices()の構成で、ReduxDevToolsMiddlewareミドルウェアを追加したサービス構成を追加するだけで実現されます。
以下が実装サンプル。

# blazor-fluxor-todo-app.Client/Startup.cs #

using Microsoft.AspNetCore.Components.Builder;
using Microsoft.Extensions.DependencyInjection;

using Blazor.Fluxor;

namespace blazor_fluxor_todo_app.Client
{
  public class Startup
  {
    public void ConfigureServices(IServiceCollection services)
    {
      services.AddFluxor(options => options
        .UseDependencyInjection(typeof(Startup).Assembly)
        .AddMiddleware<Blazor.Fluxor.ReduxDevTools.ReduxDevToolsMiddleware>()
      );
    }

    public void Configure(IComponentsApplicationBuilder app)
    {
      app.AddComponent<App>("app");
    }
  }
}

ブラウザの実行画面で Redux DevTools が有効になっています。

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

StateのChartもこのように

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

5. まとめ

という事で、Blazor(Client Side)はまだPreview版ではありますが、なかなか実践的な面白い実装ができる環境は整ってきている感じがしています。
Flux/Reduxな実装方針のほかにMVVMな実装方針なども取れるでBlazorの成長、そしてGAを期待しています^^

BlazorからWebAPIを呼び出す

1. はじめに

まず、この記事はCliend side Blazorを前提とします。
Blazorアプリケーションは、つまりSPAなアプリですので、たいていの場合 データの表示・更新等のため に Web API呼び出し が必要になります。
今回は OpenWeatherMap が提供するWeb APIをBlazorから呼び出してみたいと思います。

サンプルコード: https://github.com/ryuichi111/BlazorExamples/tree/master/web_api_blazor

1.1. OpenWeatherMap API

OpenWeatherMap(https://openweathermap.org/)は気象予報情報を提供してくれるサイトで、APIも提供しています。
アカウントの登録を行いAPI Keyを取得すれば無料の「Freeプラン」でも、呼び出し回数制限や利用できるAPIの制限がありますが、十分に利用することができます。

https://openweathermap.org/price

Sign Upを行うとメールで API Key が送信されてきます。
また、サインインした状態で「https://home.openweathermap.org/」にアクセスし「API Keys」メニューをクリックする事でも API Key を確認することができます。

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

利用するAPI

Current weather data(https://openweathermap.org/current)を利用します。
Locationを指定することで気象予報を取得することができます。

上記リンク先にも説明がありますが、HTTP GETで以下のようなリクエストを行います。

http://api.openweathermap.org/data/2.5/forecast?q=Tokyo&units=metric&APPID=[API Key]

※ q: Locationを指定
  units=metric: 気温の単位を℃に設定するオプション
  APPID: API Key

すると以下のようなJSONデータが返却されます。

{
  "cod": "200",
  "message": 0.0063,
  "cnt": 40,
  "list": [
    {
      "dt": 1556949600,
      "main": {
        "temp": 25.31,
        "temp_min": 25.31,
        "temp_max": 25.99,
        "pressure": 1017.52,
        "sea_level": 1017.52,
        "grnd_level": 1013.68,
        "humidity": 22,
        "temp_kf": -0.68
      },
      "weather": [
        {
          "id": 801,
          "main": "Clouds",
          "description": "few clouds",
          "icon": "02d"
        }
      ],
      "clouds": {
        "all": 12
      },
      "wind": {
        "speed": 2.38,
        "deg": 145.83
      },
      "sys": {
        "pod": "d"
      },
      "dt_txt": "2019-05-04 06:00:00"
    },
    {
      "dt": 1556960400,
      "main": {
        "temp": 23.88,
        "temp_min": 23.88,
        "temp_max": 24.39,
        "pressure": 1018.58,
        "sea_level": 1018.58,
        "grnd_level": 1014.49,
        "humidity": 31,
        "temp_kf": -0.51
      },
      "weather": [
        {
          "id": 804,
          "main": "Clouds",
          "description": "overcast clouds",
          "icon": "04d"
        }
      ],
      "clouds": {
        "all": 99
      },
      "wind": {
        "speed": 4.72,
        "deg": 157.488
      },
      "sys": {
        "pod": "d"
      },
      "dt_txt": "2019-05-04 09:00:00"
    },
    ...省略
    
  ],
  "city": {
    "id": 1850147,
    "name": "Tokyo",
    "coord": {
      "lat": 35.6828,
      "lon": 139.759
    },
    "country": "JP",
    "population": 8336599
  }
}

2. サンプルを作成

早速サンプルプログラムを作成していきます。

完成イメージは以下の通りです。

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

  • 「Your API Key」テキストボックス:
    OpenWeatherMapにアクセスするためのAPI Keyを入力します。
  • 「Location」テキストボックス:
    気象情報を取得する地名を入力します。
  • 「検索」ボタン:
    OpenWeatherMap APIを呼び出し、結果を表形式で表示します。

2.1. プロジェクトを作成

テンプレートは Client-side Blazor(blazor)、プロジェクト名は web_api_blazor とします。

dotnet new blazor -o web_api_blazor

2.2. 余分な実装を削除

今回は Pages/index.razor に画面実装を行うこととします。
自動生成された以下のファイルは削除します。

  • Pages/Counter.razor
  • Pages/FetchData.razor

また Shared/NavMenu.razor も余分なメニューリンクを削除します。

# 修正後の Shared/NavMenu.razor #

<div class="top-row pl-4 navbar navbar-dark">
  <a class="navbar-brand" href="">web_api_blazor</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>
  </ul>
</div>

@functions {
  bool collapseNavMenu = true;

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

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

2.3. モデルクラスの作成

OpenWeatherMapから返却されるJSONデータは前述で記載しましたが、これをC#で扱いやすくするために C#のPOCO としてモデルクラスを定義します。

(1) quicktype

JSONからC#クラスを生成する方法は色々あると思いますが、ここでは quicktype を使いたいと思います。

https://app.quicktype.io/

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

左上の「Name」には、モデルクラス名を設定します。ここでは WeatherResult としました。
そのすぐ下に JSON データをペーストします。
右端のLanguage設定パネルで、出力コード設定を行います。

  • Language: C#
    言語はもちろんC#に設定します。
  • Use T or List: List
    配列データを
    とするか List<> とするかの設定は List にします。
  • Outpupt features: Just Types
    Complete / Attributes Onlyという選択肢もありますが、これらは Newtonsoft.Json ベースのクラスを出力します。ここではプレーンなクラスが欲しかったので、Just Types を選択しました。

真ん中の背景が白い領域にコードが出力されます。
Copy Codeボタンをクリックすることで、これをクリップボードにコピーすることが出来ます。

(2) Models/WeatherResult.csを作成

web_api_blazorプロジェクトに Models/WeatherResult.cs を作成します。
quicktypeで作成したコードを利用しますが、そのままでは都合の悪い点がいくつかあるので、手動で修正します。

  • 必要なusing宣言を追加
  • namespace宣言を追加
  • 「public partial class List」を「public partial class WeatherSummary」に修正
  • enum定義を削除(合わせてそれらの型定義を使っているプロパティをstringに変更)
  • 「_」が省略されてしまったプロパティ名を修正(例:DtTxt → Dt_Txt)
  • DtJpプロパティを追加(日付情報Dt_TxtはUTCなのでTokyo時間に変換するgetterプロパティ)

以下が、修正を行った Models/WeatherResult.cs です。

# Models/WeatherResult.cs #

using System;
using System.Collections.Generic;

namespace web_api_blazor.Models {
    public partial class WeatherResult
  {
    public long Cod { get; set; }
    public double Message { get; set; }
    public long Cnt { get; set; }
    public List<WeatherSummary> List { get; set; }
    public City City { get; set; }
  }

  public partial class City
  {
    public long Id { get; set; }
    public string Name { get; set; }
    public Coord Coord { get; set; }
    public string Country { get; set; }
    public long Population { get; set; }
  }

  public partial class Coord
  {
    public double Lat { get; set; }
    public double Lon { get; set; }
  }

  public partial class WeatherSummary
  {
    public long Dt { get; set; }
    public MainClass Main { get; set; }
    public List<Weather> Weather { get; set; }
    public Clouds Clouds { get; set; }
    public Wind Wind { get; set; }
    public Sys Sys { get; set; }
    public DateTime Dt_Txt { get; set; }
    public DateTime DtJp { get { return this.Dt_Txt.AddHours(9); } }
    public Rain Rain { get; set; }
  }

  public partial class Clouds
  {
    public long All { get; set; }
  }

  public partial class MainClass
  {
    public double Temp { get; set; }
    public double TempMin { get; set; }
    public double TempMax { get; set; }
    public double Pressure { get; set; }
    public double SeaLevel { get; set; }
    public double GrndLevel { get; set; }
    public long Humidity { get; set; }
    public double TempKf { get; set; }
  }

  public partial class Rain
  {
    public double The3H { get; set; }
  }

  public partial class Sys
  {
    public string Pod { get; set; }
  }

  public partial class Weather
  {
    public long Id { get; set; }
    public string Main { get; set; }
    public string Description { get; set; }
    public string Icon { get; set; }
  }

  public partial class Wind
  {
    public double Speed { get; set; }
    public double Deg { get; set; }
  }

  //public enum Pod { D, N };

  //public enum Description { BrokenClouds, ClearSky, FewClouds, LightRain, OvercastClouds, ScatteredClouds };

  //public enum MainEnum { Clear, Clouds, Rain };
}

2.3. Pages/index.razor の実装

Pages/index.razorにメインの画面の実装を行います。

以下にペタッと完成した実装を貼り付けますが、コードの下でポイントの解説を行っています。

# すべての実装を行った Pages/index.razor #

@page "/"
@using web_api_blazor.Models
@inject HttpClient httpClient

<h1>OpenWeatherMap で お天気情報検索</h1>

<div class="form-group">
  <label for="text1">Your API Key:</label>
  <input type="text" class="form-control" bind="@AppID" />
</div>
<div class="form-group">
  <label for="passwd1">Location:</label>
  <input type="text" class="form-control" bind="@Location" />
</div>
<div class="form-group">
  <input type="button" onclick="@Search" value="検索" />
</div>
<hr />
@if( WeatherResult != null ) {
  <div>CityName: @WeatherResult.City.Name</div>
  <table class="table">
    <thead>
      <tr>
        <th scope="col">日時</th>
        <th scope="col">気温</th>
        <th scope="col">気圧(hPa)</th>
        <th scope="col">天気</th>
      </tr>
    </thead>
    <tbody>
    @foreach (var summary in WeatherResult.List)
    {
      <tr>
        <th scope="row">@summary.DtJp</th>
        <td>@summary.Main.Temp</td>
        <td>@summary.Main.Pressure</td>
        <td>
          @summary.Weather[0].Main
          (
          @summary.Weather[0].Main
          )
        </td>
      </tr>
    }
    </tbody>
  </table>  
}

@functions {
  private string AppID { get; set; }
  private string Location { get; set; } = "Tokyo";
  private WeatherResult WeatherResult { get; set; } = null;

  private async void Search() {
    string url = $"http://api.openweathermap.org/data/2.5/forecast?q={Location}&units=metric&APPID={AppID}";
    this.WeatherResult = await httpClient.GetJsonAsync<WeatherResult>(url);
    this.StateHasChanged();
  }
}

(1) @inject HttpClient httpClient

3行目で HttpClient をインジェクトしています。
このページでは、Web API呼び出しを行うために HttpClient をDIしています。
「HttpClient」はBlazorではデフォルトサービスであるため、Startup#ConfigureServices()で初期登録することなく利用することができます。
HttpClientのライフサイクルは Singleton です。

「@inject HttpClient httpClient」と宣言したため、@functionsコードブロックでは「httpClient」としてインスタンス参照することができます。

参考:https://docs.microsoft.com/en-us/aspnet/core/blazor/dependency-injection?view=aspnetcore-3.0

(2) API Key / Location 入力項目

<inout type="text>タグでAPI Key / Locationの入力項目を定義しています。
「UIによる入力 → AppID / Localtionプロパティ」というデータバインディングが必要なので「bind="@xxxx"」とします。つまり、two-wayのバインディング設定を行っています。

(3) Search() / httpClient.GetJsonAsync<T>()

検索ボタンのクリック時にSearch()メソッドが呼び出されます。
UIによる入力項目を利用し、OpenWeatherMap APIを呼び出すURLを作成します。

string url = $"http://api.openweathermap.org/data/2.5/forecast?q={Location}&units=metric&APPID={AppID}";

HTTPリクエストの実行には httpClient を利用し、GetJsonAsync<T>()メソッドを利用します。

this.WeatherResult = await httpClient.GetJsonAsync<WeatherResult>(url);

JSON文字列をそのまま受け取りたい場合は httpClient.GetStringAsync(url) を呼び出します。

GetJsonAsync<T>() は、内部的に GetStringAsync() を呼び出し JsonUtil.Deserialize(json) を行っています。

https://github.com/aspnet/Blazor/blob/8c9910743959b16d7ff3b2fbb1014fe000a716e4/src/Microsoft.AspNetCore.Blazor/Json/HttpClientJsonExtensions.cs#L23

(4) StateHasChanged()

コンポーネントの状態変更をマニュアル通知し、画面の再レンダリングを行います。

(5) レンダリング定義

「div / table / tr / td 等」+「@xx Razor構文」で、WeatherResultを表形式でレンダリングしています。
CSSはbootstrapのもので、C#コードはRazor構文で記述します。

2.4. 実行

web_api_blazorディレクトリで dotnet run コマンドを実行して、ブラウザで http://localhost:5000 にアクセスします。

3. まとめ

ということで、BlazorからWeb APIの呼び出しを試してみました。
デフォルトサービスとして用意されている HttpClient を使う、という事のみが重要なポイントでした。
HttpClientクラスは、C#経験者なら馴染み深いものなので、これは問題ないですね。
モデルクラスの用意に関しては、今回はJSONデータのみが手に入る状態からC#コードの生成を行うため、quicktypeを利用しました。この辺りはBlazorに限ったことではなく、必要に応じてケースバイケースでの用意になるかと思います。
また、API Keyを画面で入力してBlazorクライアントから直接OpenWeatherMap APIを呼び出す実装としました。これについては、自前のWeb APIでラップする等のアーキテクチャはサンプルという事で無視しておりますのであしからずm( )m

BlazorでSPAするぞ!(7) - DI(Dependency Injection)

※ 2019/6/15 .NET Core 3.0 Preview 6 に対応した記述に修正しました。

という事で、↓↓↓の続きです。

ryuichi111std.hatenablog.com

今回はDI(Dependency Injection)について。
まあ、今どきのフレームワークなので(そして.NET Coreの流れをくむBlazorなので)DIは標準でサポートしています。

DIって当たり前に使う時代になりすぎてて、改めてwikiを見てみました。

依存性の注入 - Wikipedia

Dependency injection - Wikipedia

Inversion of control - Wikipedia

DIという言葉を最初に提唱したのマーティン・ファウラー氏だったかぁ。。
IoCの起源は1988年かぁ。。。
とか意味ないこと書いてしまった。。。

1. DI(Dependency Injection)

DI(Dependency Injection)は「依存性の注入」ですね。
「依存性の注入って言われても意味わからん」が、DIがメジャーになった2000年初期頃からの定番な話ですが。。。

オブジェクトの具象クラスに対してプログラミング(操作)するのではなく、もっと抽象的なインターフェイスに対して操作を行おう。
new オブジェクト() とかしないで、インターフェイスを実装したオブジェクトの生成をDIフレームワークにやってもらおう。
プログラムからは具象オブジェクトは意識しない。

的なものですね。

DIによるメリットの代表は、以下のようなものでしょうか。

  • オブジェクト間の依存性・結合度が低くなる
  • テストしやすい(モックに差し替えやすい)
  • (上記にも重なるが)振る舞いの差し替えが容易

ここではDI自体の説明には注力しないので、このへんで。

という事で Blazor におけるDIの利用方法の説明に移ります。

まず、サンプルプロジェクトを作成しておきます。

dotnet new blazor -o di_blazor

以下のような実装を行うこととします。

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

消費税計算を行うサンプルです。
税抜価格をテキストボックスに入力し、「税込価格計算」ボタンをクリックすると、税込価格が計算され画面に表示されます。
消費税計算を行う処理をDIでインジェクトするクラスとします。

2. サービスを作成

BlazorのDIはシンプルで全体の流れとしては以下のようになります。

  • 初期化時にInterfaceおよび対応する具象クラスをDIに登録する
  • オブジェクトを使いたい箇所で、Blazorフレームワークによってインターフェイスオブジェクトとしてインジェクトしてもらう

まずはDIでインジェクトしてもらう対象のサービスクラスを用意します。
消費税計算サービスクラスを作成します。そしてこのオブジェクトを、インジェクトの対象とします。
税抜価格をインプットとして、税込価格をアウトプットしてくれるクラスを作成します。また、2019/10/1以降であれば税率10%を適用します。

2.1. インターフェイス実装

以下はIConsumptionTaxCalculatorインターフェイス実装。

# Services/IConsumptionTaxCalculator.cs #

namespace di_blazor.Services {
  public interface IConsumptionTaxCalculator {
    int CalcTotalPrice(int price);
  }
}

2.2. 具象クラス実装

以下がConsumptionTaxCalculator具象クラス実装。

# Services/ConsumptionTaxCalculator.cs #

using System;

namespace di_blazor.Services {
  public class ConsumptionTaxCalculator : IConsumptionTaxCalculator {
    public int CalcTotalPrice(int price) {
      double rate = 0;
      if( DateTime.Today >= new DateTime(2019, 10, 1) )
        rate = 1.1;
      else
        rate = 1.08;
      return (int)(price * rate);
    }
  }
}

3. サービスを登録

「IConsumptionTaxCalculator / ConsumptionTaxCalculator」をBlazorのDIに登録します。
Startupクラスの ConfigureServices(IServiceCollection services)メソッド でオブジェクトを登録します。
以下のように「services.AddSingleton<T, O>()」で、インターフェイスとオブジェクトの組み合わせでDI対象のオブジェクトを登録します。

# Startup.cs #

...省略...
using di_blazor.Services;

namespace di_blazor
{
  public class Startup
  {
    public void ConfigureServices(IServiceCollection services)
    {
      services.AddSingleton<IConsumptionTaxCalculator, ConsumptionTaxCalculator>();
    }
  ...省略...
  }
}

3.1. DIオブジェクトのライフサイクル

上記サンプルでは「AddSingleton()」メソッドを使用しましたが、他に「AddTransient()」メソッドがあります。
名称から大体想像が付きますが、インジェクトされるオブジェクトの生存期間の違いがあります。
それぞれのライフサイクルは以下の通りです。

ASP.NET CoreのDI存在する「Scoped」はClient side Blazorでは未対応です。

サービスクラスの持つフィールド・プロパティの状態管理の性格に応じて Singleton / Transient を使い分けます。

4. コンポーネントにサービスをインジェクト

4.1. .razorにインジェクト

Pages/Index.razorを改造して、ここに IConsumptionTaxCalculator をインジェクトします。

# Pages/Index.razor #

@page "/"
@using di_blazor.Services
@inject IConsumptionTaxCalculator calculator

税抜価格:<input type="text" bind="@Price" />
<br />
<input type="button" onclick="@Calc" value="税込価格計算" />
<br />↓↓↓↓<br />
<div>税込価格:@TaxedPrice 円</div>
@code {
  private int Price { get; set; }
  private int TaxedPrice { get; set; }

  public void Calc() {
    this.TaxedPrice = calculator.CalcTotalPrice(this.Price);
  }
}

【@inject】

「@inject」ディレクティブを利用してオブジェクトをインジェクトします。
以下の書式になります。

@inject 【インジェクトするオブジェクトの型】 【インジェクトする変数名】

@using により IConsumptionTaxCalculator を実装した名前空間「di_blazor.Services」を参照している事にも注意してください。

【コードブロック】

@code コードブロックでは、calculator としてインジェクトされたオブジェクトを利用することができます。
ちなみに、このコードをコンパイルしたILを、逆コンパイルすると以下のようになります。

# Indexクラス #
...省略...

[Layout(typeof(MainLayout))]
[Route("/")]
public class Index : ComponentBase
{
  ...省略...

  [Inject]
  private IConsumptionTaxCalculator calculator
  {
    get;
    set;
  }

  ...省略...

  public void Calc()
  {
    TaxedPrice = calculator.CalcTotalPrice(Price);
  }
}

Index.razorから生成された Indexクラス のプロパティとして「IConsumptionTaxCalculator calculator」が定義され、[Inject]属性が付与されています。
DIされるのでIndexクラス内自体にはインスタンス生成ロジックは存在しません。

4.2. .razorのコードビハインドにインジェクト

.razorの@codeコードブロックではなく、コードビハインドクラスを利用した場合の実装は以下の通りです。
先程、@injectを利用した .razor ファイルがコンパイルされるとどのようなILになるかを見ましたが、ほぼほぼ それですね。。。
プロパティに[Inject]属性を付けてあげると、いわゆる プロパティ インジェクション によりオブジェクトが割り当てられます。

# Pages/Index.razor #

@page "/"
@inherits IndexBase

税抜価格:<input type="text" bind="@Price" />
<br />
<input type="button" onclick="@Calc" value="税込価格計算" />
<br />↓↓↓↓<br />
<div>税込価格:@TaxedPrice 円</div>
# Pages/IndexBase.cs(コードビハインドクラス) #

using System;
using Microsoft.AspNetCore.Components;
using di_blazor.Services;

namespace di_blazor.Pages
{
  public class IndexBase : ComponentBase
  {
    [Inject]
    private IConsumptionTaxCalculator calculator { get; set; }

    protected int Price { get; set; }
    
    protected int TaxedPrice { get; set; }

    public void Calc() {
      this.TaxedPrice = calculator.CalcTotalPrice(this.Price);
    }
  }
}

5. サービスクラスへのインジェクション

では、話を少し別のケースに進めます。
上記までで、.razor(もしくはコードビハインドクラス)へのDIは、@injectディレクティブで定義することができ、裏側ではプロパティ インジェクションが動作することが分かりました。
本投稿シリーズレベルのサンプル実装では、.razorのみ もしくは (ほぼエンティティな)モデルクラス程度しか利用していませんでした。ただし、もっと大きめのアプリケーションをBlazorで作成する場合、適切な責務を持ったモデルクラスやサービスクラスのようなクラス設計・実装が必要になります。
そのようなクラス(つまり .razor 以外)でDIを利用する方法について見てみましょう。

結論としては、これは ASP.NET CoreのDIと同じです。

サンプルでは、IConsumptionTaxCalculator / ConsumptionTaxCalculatorクラスがDateTimeに依存しています。
つまりシステム日時に依存した動作しかできない。
という事で、IDateTimeProvider / DateTimeProviderクラスを用意し、ConsumptionTaxCalculatorにはコレをインジェクトします。

5.1. IDateTimeProvider / DateTimeProviderクラスを作成

Servicesフォルダ配下に実装します。

# Services/IDateTimeProvider.cs #

using System;

namespace di_blazor.Services {
  public interface IDateTimeProvider {
    DateTime Today();
  }
}
# Services/DateTimeProvider.cs #

using System;

namespace di_blazor.Services {
  public class DateTimeProvider : IDateTimeProvider {
    public DateTime Today() {
      return DateTime.Today;
    }
  }
}

上記に加え、IDateTimeProviderの実装を増やすことで、DateTimeに依存しない Mockクラス を用意する事ができます(本投稿ではそこまでの実装は行わずに進めます)。

5.2. IDateTimeProvider / DateTimeProviderを登録

Startupクラスに、IDateTimeProvider / DateTimeProviderを登録します。
これは先程のIConsumptionTaxCalculator / ConsumptionTaxCalculatorと同様です。

# Startup.cs #

...省略...
using di_blazor.Services;

namespace di_blazor
{
  public class Startup
  {
    public void ConfigureServices(IServiceCollection services)
    {
      services.AddSingleton<IDateTimeProvider, DateTimeProvider>();
      services.AddSingleton<IConsumptionTaxCalculator, ConsumptionTaxCalculator>();
    }
    ...省略...
  }
}

5.3. ConsumptionTaxCalculatorに、IDateTimeProviderをインジェクト

ConsumptionTaxCalculatorクラスにIDateTimeProviderオブジェクトをインジェクトします。
.razorではなく、サービスクラス(コンポーネントクラス以外)にインジェクトする際は「コンストラクタ インジェクション」の形式をとります。
以下が実装になります。

# Services/ConsumptionTaxCalculator.cs #

using System;

namespace di_blazor.Services {
  public class ConsumptionTaxCalculator : IConsumptionTaxCalculator {
    private IDateTimeProvider _DateTimeProvider = null;

    public ConsumptionTaxCalculator(IDateTimeProvider dateTimeProvider) {
      this._DateTimeProvider = dateTimeProvider;
    }

    public int CalcTotalPrice(int price) {
      double rate = 0;
      if( this._DateTimeProvider.Today() >= new DateTime(2019, 10, 1) )
        rate = 1.1;
      else
        rate = 1.08;
      return (int)(price * rate);
    }
  }
}

【コンストラクタ引数】

「public ConsumptionTaxCalculator(IDateTimeProvider dateTimeProvider)」という形で、サービスクラスのコンストラクタの引数でDI対象の「IDateTimeProvider」オブジェクトを引数として受け取っています。
AddSingleton()やAddTransient()で登録済みのインターフェイスオブジェクトは、BlazorのDIフレームワークによってインスタンス生成時に、コンストラクタ引数としてインスタンスが渡される仕組みになっています。
ここでは、別のメソッド(CalcTotalPrice(int price))が呼び出された際に利用するため、自らのフィールド変数にオブジェクトを代入して利用しています。

6. デフォルトサービスとカスタムサービス

BalzorのDIの使い方は、すでに説明した方法になります。

インジェクトするオブジェクトには、「デフォルトサービス」と「カスタムサービス」という2つの種類があります。

すでに説明した ConsumptionTaxCalculator や DateTimeProvider をインジェクトするのは カスタムサービス という分類になります。
これとは別に「デフォルトサービス」というものがあります。
名前の通りデフォルトで用意されたサービスになり、これらをインジェクトすることができます。
以下の3つがデフォルトサービスとして用意されており、これらは明示的な登録(AddSingleton() / AddTransient())を行うことなく .razorコンポーネントやサービスクラスにインジェクトすることができます。

  • HttpClient
    Singletonで提供されます。
    Http通信を行うオブジェクトです。BlazorアプリからサーバのWebAPIを呼び出し時などに利用します。
  • IJSRuntime
    Singletonで提供されます。JavaScriptとの相互運用に利用します(Blazor C#からのJavaScript呼び出し等)。
  • IUriHelper
    Singletonで提供されます。URI/Nabigation状態管理ヘルパー。

ここら辺を使ったサンプルは、後々 別途ブログにあげようと思います。

7. まとめ

「BlazorでSPAするぞ!」という事で、いくつかの投稿で Blazorの基本 を歩いてきました。
まだPreview版という事で今後、仕様的な変更、改善が行われるのだと思いますが、リリースに備えて自分自身で基本を理解・整理することができたと思います。
「BlazorでSPAするぞ!」というシリーズ投稿としては、これで完結し、今後の投稿ではTips的にBlazor関連情報を書いていこうかと思います。

では、Let's enjoy Blazor together!!!