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) -正式版対応済

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

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

ryuichi111std.hatenablog.com

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

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

依存性の注入 - Wikipedia

Dependency injection - Wikipedia

Inversion of control - Wikipedia

DIという言葉を最初に提唱したのマーティン・ファウラー氏だったかぁ。。
IoCの起源は1988年かぁ。。。
とか、おっさんエンジニアは歴史と共にDI/IoCに思いをめぐらせますね。。。
ということで本題。

1. DI(Dependency Injection)

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

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

的なものですね。

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

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

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

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

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

dotnet new blazorwasm -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に登録します。
ProgramクラスのMain()メソッド でオブジェクトを登録します。
以下のように「services.AddSingleton<T, O>()」で、インターフェイスとオブジェクトの組み合わせでDI対象のオブジェクトを登録します。

# Program.cs #

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

namespace di_blazor
{
  public class Program
  {
    public static async Task Main(string[] args)
    {
      var builder = WebAssemblyHostBuilder.CreateDefault(args);
      
      builder.Services.AddSingleton<IConsumptionTaxCalculator, ConsumptionTaxCalculator>();
      
      ...省略
    }
  }
}

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

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

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

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

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

4.1. 【方法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クラス #
...省略...

[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. 【方法2】.razorのpartialコードビハインドクラスにインジェクトする

.razorの@codeコードブロックではなく、partialとして定義したコードビハインドクラスを利用した場合の実装は以下の通りです。
@Code{ }ブロックの実装を単純にIndexクラス(Index.razor.cs)に移行するだけです。
Index.razorで定義した @inject による「calculatorプロパティ」は、partialである.cs上のIndexクラスで参照可能です。

# 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>
# Index.razor.cs #

using System;

namespace di_blazor.Pages
{
  public partial class Index
  {
    private int Price { get; set; }
    private int TaxedPrice { get; set; }

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

4.3. 【方法3】.razorのComponentBase派生コードビハインドクラスにインジェクトする

次に、ComponentBase派生クラスとして実装したコンポーネントクラスにインジェクトする方法の説明になります。

ComponentBaseクラスを継承した IndexBase クラスを定義します。
Index.razorは IndexBaseクラス を継承(@inherits)する方式での実装になります。
Index.razorの定義は以下の通り。@injectはなくなり、@inherits定義を追加しています。

# Pages/Index.razor #

@page "/"
@inherits IndexBase

税抜価格:<input type="text" @bind="Price" />
<br />
<input type="button" @onclick="Calc" value="税込価格計算" />
<br />↓↓↓↓<br />
<div>税込価格:@TaxedPrice 円</div>

次にIndexBase.cs定義です。
IndexBaseクラスのプロパティ定義として明示的に [Inject]属性 を付与した「calculatorプロパティ」を定義します。
プロパティに[Inject]属性を付けてあげると、いわゆる プロパティ インジェクション によりオブジェクトが割り当てられます。

# 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を登録

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

# Program.cs #

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

namespace di_blazor
{
  public class Program
  {
    public static async Task Main(string[] args)
    {
      var builder = WebAssemblyHostBuilder.CreateDefault(args);
      
      builder.Services.AddSingleton<IDateTimeProvider, DateTimeProvider>();
      builder.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つがデフォルトサービスとして用意されています。
以下の3つがデフォルトサービスとして用意されており、これらは明示的な登録(AddSingleton() / AddTransient())を行うことなく .razorコンポーネントやサービスクラスにインジェクトすることができます。

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

「HttpClient」に関しては dotnet new blazorwasm コマンドで作成したデフォルトプロジェクトの実装において、以下のようにInjectが初期実装されています。

// Program.csのMain()内に以下の実装がデフォルトで追加されています。

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

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

7. まとめ

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

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

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

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

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

ryuichi111std.hatenablog.com

今回はルーティングについて。
URLに対するページ(コンポーネント)のルーティング(マッチング)ですね。

1. ルーティング(Routing)

サンプルプロジェクトからルーティングを学んでいきたいと思います。
まず、dotnet new blazorwasm コマンドでプロジェクトを作成します。

dotnet new blazorwasm -o route_blazor

自動生成コードには、ルーティング実装も ある程度組み込まれています。

dotnet run で実行し、http://localhost:5000 にアクセスします。
これは「/」(ルート)へのアクセスになります。
「/」に対するルーティング設定は、「Pages/Index.razor」となっています。

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

サイドメニューから「Counter」をクリックします。
ブラウザのURLが http://localhost:5000/counter になりました。
「/counter」へのアクセス、そして「/counter」へのルーティング設定は「Pages/Counter.razor」となります。

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

同様にサイドメニューから「Fetch data」をクリックします。
ブラウザのURLが http://localhost:5000/fetchdata になりました。
「/fetchdata」へのアクセス、そして「/fetchdata」へのルーティング設定は「Pages/FetchData.razor」となります。

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

1.1. @Page

「/」「/counter」「/fetchdata」のような各URLに対する .razor ファイルのルーティングは、「@Page」ディレクティブで設定されます。
自動生成された3つのコンポーネント(Pages配下の.razorファイル)の定義は、それぞれ以下の通りです。

# Pages/Index.razor #

@page "/"
...省略
# Pages/Counter.razor #

@page "/counter"
...省略
# Pages/FetchData.razor #

@page "/fetchdata"
...省略

1.2. ユーザ一覧画面を実装する場合

では、新規に「ユーザ一覧画面」を追加する想定の実装例を見てみましょう。
ルーティング設定するURLは「/users/list」とします。
.razorコンポーネントは「Pages/UserList.razor」ファイルで定義します。
以下がその定義になります。

# Pages/UserList.razor #

@page "/users/list"

ユーザ一覧画面です!!!
...省略...

1.3. URLが変わるけどSPA

自動生成されたプロジェクトを動作させるだけでもわかりますが、ページ(コンポーネント)を切り替えると、ブラウザのURLが切り替わります。
ただし、Blazorはクライアントサイドで動作しているので、対象のページ(コンポーネント)をサーバにHttpRequestするような事はせずSPAとして動作しています。
ChromeであればDevToolを表示し「Network」の監視を行えば確認することができます。

以下が実際に route_blazor を起動した後(ルートを表示した後)、「Counterメニュークリック」→「Fetch dataメニュークリック」という操作を行った際のNetwork状態です。

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

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

Counterメニュー クリック時:「favicon.ico」リクエストのみ
Fetch dataメニュー クリック時:「favicon.ico」リクエストおよび「weather.json」リクエストのみ。weather.jsonはアプリロジック的にサーバからjsonデータを明示的にリクエストしているため。

2. <Router>の初期化

@Pageディレクティブ定義によるルーティング設定方法を先に説明しましたが、アプリケーションでは<Router>を初期化しておく必要があります。
ルーティングを有効化する初期化処理は「/App.razor」で行います。
自動生成されたプロジェクトでも、以下のように既に実装が行われています。

# /App.razor #

<Router AppAssembly="typeof(Program).Assembly" />

<Router>を定義し、AppAssembly属性に「typeof(Program).Assembly」を設定します。
Routerクラスは、「Microsoft.AspNetCore.Components.Routing」名前空間で定義されています。

2.1. 無効ルーティング時の表示

定義されたルーティングにマッチしなかった場合(フォールバックした場合)に表示するコンテンツを、「Router - NotFound属性」で指定することができます。

# /App.razor #

...
<NotFound>
  <LayoutView Layout="@typeof(MainLayout)">
    <p>Sorry, there's nothing at this address.</p>
  </LayoutView>
</NotFound>
...

「<p>Sorry, there's nothing at this address.</p>」部分は任意のコンポーネントに置き換えることも可能です。
以下のように「Shared/FallbackPage.razor」ページを用意して、App.razorのNotFountコンテンツとして指定します。

# Shared/FallbackPage.razor #

@DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")
<p>無効なパスが要求されました;;</p>
# App.razor #

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <FallbackPage />
        </LayoutView>
    </NotFound>
</Router>

dotnet runで実行し、http://localhost:5000/invalid にアクセスしてみます。「/invalid」はどのコンポーネントにもルーティングされていません。
以下のように、フォールバックページ(Shared/FallbackPage.razor)が表示されました。

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

RouterのFallbackComponent属性が未設定の場合、無効なURLがアクセスされると、以下のように「Loading...」画面で止まってしまいます。

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

Chromeの「DevTools - Console」を確認すると、WASMのエラーが多数出力された状態で止まっていることが確認できます。

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

2.2. 複数のルート設定

1つのコンポーネント(ページ)には、複数のルーティング定義を行うことができます。
つまり @Page を複数設定可能という事です。
先程の「UserList.razor」に対して、2つのURLルーティング設定を行った例が以下です。

# Pages/UserList.razor #

@page "/users/list"
@page "/employees/list"

ユーザ一覧画面です!!!

3. NavLinkコンポーネント

ルーティング設定された各コンポーネント(ページ)の表示切替は <a href="/xxx"> タグで可能です。
<a>タグをラップしたコンポーネントとして「NavLinkコンポーネント(Microsoft.AspNetCore.Components.Routing名前空間)」が用意されています。
サンプルプロジェクトでは、左サイドのメニューを「Shared/NavMenu.razor」で実装しており、ここでNavLinkコンポーネントを利用しています。

# Shared/NavMenu.razor #

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

@code {
  bool collapseNavMenu = true;

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

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

3.1. href属性

<a>タグと同様に「href属性」でURLを設定します。この「URL」と「.razprで定義された@Page値」によってルーティング先コンポーネントが決定されます。

3.2. 「active」CSSクラス

NavLinkコンポーネントの特徴(機能の1つ)として、「href属性値」と「現在のURL」がマッチした場合、「active」CSSクラスが付与される というものがあります。

以下は「現在のURLが /counter」なので「Counterメニューにactive cssクラス」が付いています。
加えて、NavLinkは<a>タグとしてレンダリングされていることが確認できます。

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

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

Match属性

NavLinkコンポーネントには「Match属性(Microsoft.AspNetCore.Components.Routing名前空間のNavLinkMatch列挙型)」というものがあります。
href値とURLのマッチングルールを設定します。
以下の2つの列挙値があります。

  • NavLinkMatch.All
    URLが完全一致した場合に active CSSクラスが適用されます。
    ※ href=/usersに対しては、URL=/users はマッチするが、URL=/users/list はマッチしない
  • NavLinkMatch.Prefix
    URLのプレフィクスが一致した場合に active CSSクラスが適用されます。
    ※ href=/usersに対しては、URL=/users も URL=/users/list もマッチとして扱われる

4. ルートパラメータ

ルーティングのURLにパラメータを含めることができます。
それらのパラメータ値は、ルーティング先のコンポーネントで受け取ることができます。

Pages/Index.razorコンポーネントでルート パラメータを受け取るようにしたいと思います。

4.1. @page ディレクティブにパラメータ定義を追加

ルートパラメータは @Page ディレクティブで設定を行います。
Index.razorは「/」(ルート)にルーティングされています。
「/【Message】」といった形式でパラメータを受け取るように設定します。

# Pages/Index.razor #

@page "/"
@page "/{Message}"

<h1>Hello, world!</h1>

your message is '@Message'
<br />

@code {
  private string _Message = "None";

  [Parameter]
  protected string Message {
    get {
      return this._Message;
    }
    set { 
      this._Message = System.Net.WebUtility.UrlDecode(value);
    }
  }
}

上記のように、ルートパラメータ定義は「{【パラメータ名】} 」の形式をとります。
Messageは、コードブロックで[Parameter]属性を付与したプロパティとして定義します。
ここでは「%20(スペース)」などのURLエンコードされた文字列パラメータを、URLデコードしてMessageプロパティに保持するために、getter / setterを定義し setterではUrlDecode()処理を行っています。

dotnet run コマンドで実行し、以下のURLをブラウザでアクセスします。

http://localhost:5000/Blazor%20is%20new%20SPA%20framework

URLパラメータを受け取り、画面に表示することが出来ました。

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

4.2. ルートパラメータの型指定

ルートパラメータは厳密な型を指定することができます。
Pages/Counter.razorコンポーネントに、CurrentCountの初期値をint型ルートパラメータとして受け取るような実装を追加したいと思います。
以下のように「{【パラメータ名】:【型名】}」の形式でルーティング設定を記述します。
コードブロック側も、[Parameter]属性を適用するために、CurrentCountを 変数 から setterを持つプロパティ に修正しています。

# Pages/Counter.razor #

@page "/counter"
@page "/counter/{currentCount:int}"

<h1>Counter</h1>

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

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

@code {
  [Parameter]
  public int CurrentCount {get; set; } = 0;

  void IncrementCount()
  {
    this.CurrentCount++;
  }
}

http://localhost:5000/counter/100」にアクセスした実行画面は以下です。

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

5. まとめ

ルーティングも従来のASP.NET Core等の仕組みと基本思想は同じくしているので、スムーズに理解することができたました。
次の投稿では、DI(Dependency Injection)を見ていこうと思います。

次は ryuichi111std.hatenablog.com

BlazorでSPAするぞ!(5) - レイアウト -正式版対応済

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

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

ryuichi111std.hatenablog.com

今回はレイアウト機能について。

1. レイアウト機能とは

これはよくあるやつですね。
ASP.NET Coreでの「_Layout.cshtml」と大体同じことです。
ヘッダやフッター、メニューなど、アプリケーション内の複数のページで共通する画面項目(UI)がある場合に使われます。
すべての画面定義に「ヘッダ、フッター、メニュー」をペタペタとコピペしても画面実装は可能ですが、煩雑ですしメンテナンス性も失われます。
そこで、複数画面で共通して利用するUIを「レイアウト」として定義します。そして、画面毎に異なるコンテンツ部分を個別に実装し、それらを共通レイアウトに差し込む事ができます。

2. 自動生成コードから学ぶ

dotnet new コマンドで自動生成したプロジェクトは、きっちりとレイアウト機能が使われた形になっています。
自動生成コードを見ながらレイアウト機能を理解していきたいと思います。

では、dotnet new blazorwasm コマンドでプロジェクトを生成します。

dotnet new blazorwasm -o layout_blazor

2.1. _Imports.razor / @layout

まず「_Imports.razor」という名前のファイルがBlazorにおいて特殊なファイルとして扱われます。
コンパイラは「
_Imports.razor」という名前のファイルを見つけると、このファイルの記述内容を同一フォルダおよび配下のフォルダに再帰的に適用します。

(1) /_Imports.razor

layout_blazorプロジェクトでは、まず、プロジェクトルートに「__Imports.razor」ファイルがあります。

# /_Imports.razor #

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using layout_blazor
@using layout_blazor.Shared

「@using」宣言がいくつか並んでいます。
ルートの_Imports.razorで定義されているという事で、つまり、このプロジェクトではルートフォルダおよび配下フォルダで定義されるすべての .razor において、これらのusingが有効になります。

(2) DefaultLayout

App.razorを見ると、Routerが定義されていますが、その中で「DefaultLayout」値が設定されています。
以下のように「DefaultLayout="@typeof(MainLayout)」と定義されています。
つまり、このアプリケーション内では「MainLayout」がデフォルトのレイアウトとして適用されます。

# App.razor #

<Router AppAssembly="@typeof(Program).Assembly">
    <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>

(3) MainLayout

MainLayoutは、どこで定義されているでしょうか?
これは、Sharedフォルダ配下で定義されています。
App.razorにおいて「MainLayout」と記述できる理由は、/_Imports.razorにおいて「@using layout_blazor.Shared」という宣言が行われていたためです。

以下を見てわかるように、この「Shared/MainLayout.razor」がレイアウト定義を行っているファイルになります。

# Shared/MainLayout.razor #

@inherits LayoutComponentBase

<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>
【@inherits LayoutComponentBase】

@inheritsディレクティブは、コードビハインドクラスを指定するときにも使用しましたが、ここでは MainLayout の継承元として LayoutComponentBase を指定しています。
LayoutComponentBaseクラスは、Blazorライブラリで「Microsoft.AspNetCore.Components」名前空間に定義されたクラスです。
レイアウトを定義するクラスは「LayoutComponentBase」を継承する必要があります。

【NavMenu】

サイドバーとして「NavMenu」コンポーネントを配置しています。
NavMenu.razorは、同じSharedフォルダに定義されており、以下のような実装になります。
実行したサンプルの 左のサイドバーメニュー そのままの定義ですね。

# Shared/NavMenu.razor #

<div class="top-row pl-4 navbar navbar-dark">
  <a class="navbar-brand" href="">layout_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>
    <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;
  }
}
【@Body】

「@Body」ディレクティブが、各ページ(各コンポーネント)固有のコンテンツが配置される部分になります。
例えば「/」(ルート)にマップされるファイルは Pages/Index.razor になりますが、Shared/MainLayout.razor の @Body 箇所に Pages/Index.razor がレンダリングされます。

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

3. まとめ

レイアウト機能は比較的シンプルでASP.NET Coreのレイアウトなどを知っていれば、スムーズに理解できそうです。
自動生成されるプロジェクトが、昔に比べて複雑化していますが、これを丁寧にトレースすることで、レイアウト機能の基本は理解できると思います。

次は ryuichi111std.hatenablog.com

BlazorでSPAするぞ!(4) - データバインド(おかわり) - 正式版対応済

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

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

ryuichi111std.hatenablog.com

1. データバインド

Component のデータバインディングについては BlazorでSPAするぞ!(2) - Component -正式版対応済 - ryuichi111stdの技術日記 で軽く説明し、value="@xxx"によるone-wayバインディング / @bind="xxx"によるtwo-wayバインディングについても使用しました。
この部分についてもう少し おかわり(深堀) したいと思います。

1.1. サンプル

以下のような画面のサンプルアプリをもとに説明を進めます。

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

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

dotnet new blazorwasm -o bind_ex_blazor

Components/ChildComponent.razorコンポーネントを作成します。

# Components/ChildComponent.razor #

<div class="container bg-secondary">
  <h2>Child Component</h2>

  <p>YourScore: @YourScore</p>
  <input type="text" value="@YourScore" />
</div>

@code {
    [Parameter]
    public int YourScore { get; set; }
}

YourScoreというint型の値をパラメータとして受け取り、テキストおよびinput要素として表示します。

次に、ChildComponentコンポーネントをPages/Index.razorに配置します。

# Pages/Index.razor #

@page "/"
@using bind_ex_blazor.Components

<div class="container bg-primary">
  <h2>Index Component</h2>
  Score: @YourScore
  <br />
  <button @onclick="InclementScore">+</button>
  <button @onclick="DeclementScore">-</button>
  <br />
  <ChildComponent YourScore="@YourScore" />
</div>

@code {
  private int YourScore = 100;

  private void InclementScore() {
    this.YourScore++;
  }

  private void DeclementScore() {
    this.YourScore--;
  }
}

Index.razor自体にも いくつかUIを追加しています。
YourScoreは初期値100で用意しています。
+ボタン / -ボタンを配置し、クリックされたらイベントハンドラ「InclementScore() / DeclementScore()」によりYourScoreの値を増減させます。

1.2. 動作確認

では、動作を確認していきます。

+ボタンをクリックすると、Index.razorの YourScore 値が +1 され 101 になります。
-ボタンをクリックすると、Index.razorの YourScore 値が -1 され 99 になります。

以下は 110 までincrementした状態。

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

Index.razorの「Score: @YourScore」の値も、Blazor Runtimeによりバインド変数の変化の影響を自動判定して変更が反映されます。
また、ChildComponentへの「YourScore="@YourScore"」の値も、同様に変更が反映します。

以下、引き続きこのサンプルをベースに説明を進めます。

2. value="@YourScore" によるデータバインド

上述サンプルにおいて、ChildComponentのテキストボックスの値を直接変更してみます。
「1012」を入力し、フォーカスを移動します。

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

テキストボックス以外のYourScore値は変更されませんした。
これはテキストボックスに対して「value="@YourScore"」としてデータバインド定義が行われており、つまりone-wayバインディングになっているためです。
(データ→UIの一方通行バインディング)

3. @bind="YourScore" によるデータバインド

ChildComponent.razorを以下のように「@bind="YourScore"」で書き換えます。
これでtwo-wayバインディングとなり、データ→UI / UI→データの両方向のデータバインディングが有効になります。

# Components/ChildComponent.razor #

<div class="container bg-secondary">
  <h2>Child Component</h2>

  <p>YourScore: @YourScore</p>
  <input type="text" @bind="YourScore" />
</div>

@code {
    [Parameter]
    public int YourScore { get; set; }
}

dotnet runし直し、再び、ChildComponentのテキストボックスの値を直接変更してみます。
two-wayバインディングになったため、UIでの入力内容 1012 が裏側でChildComponentの YourScoreプロパティ値 に反映し、「YourScore: @YourScore」にも変更が反映しました。

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

ただし、親コンポーネント側(Index.razor)にはこの変更は伝達されていません(Index.razorのScore表示は100のまま)。

4. @bind-YourScore="YourScore" によるデータバインド

ChildComponentでのYourScoreの変更が、親コンポーネントIndex.razorにも伝達されるようにしてみたいと思います。

4.1. Index.razorのバインド定義を修正

コンポーネント側(Index.razor)で、ChildComponent定義時のYourScoreプロパティへのパラメータ定義を「YourScore="@YourScore"」から「@bind-YourScore="@YourScore"」に変更します。
つまり「@bind-」プレフィックスを追加しています。

@page "/"
@using bind_ex_blazor.Components

<div class="container bg-primary">
  <h2>Index Component</h2>
  Score: @YourScore
  <br />
  <button @onclick="InclementScore">+</button>
  <button @onclick="DeclementScore">-</button>
  <br />
  <ChildComponent @bind-YourScore="@YourScore" />
</div>

@code {
  private int YourScore = 100;

  private void InclementScore() {
    this.YourScore++;
  }

  private void DeclementScore() {
    this.YourScore--;
  }
}

4.2. ChildComponent.razorにEventCallbackを実装

ChildComponent.razor側にも手を加えます。

# Components/ChildComponent.razor #

<div class="container bg-secondary">
  <h2>Child Component</h2>

  <p>YourScore: @YourScore</p>
  <input type="text" @bind="YourScore" />
</div>

@code {
    [Parameter]
    public EventCallback<int>YourScoreChanged { get; set; }

    private int _yourScore = 0;

    [Parameter]
    public int YourScore {
        get {
            return this._yourScore;
        }
        set {
            if(this._yourScore != value) {
                this._yourScore = value;
                YourScoreChanged.InvokeAsync(this._yourScore);
            }
        }
    }
}

(1) EventCallbackパラメータ定義

EventCallbackパラメータを追加しています。
YourScoreChangedイベントを定義していますが、これにより当該イベント発生時にサブスクライバに対してイベントコールバックを呼び出すことができます。
int型の値をコールバック引数として渡すので「EventCallback<int>」と定義しています。

(2) YourScoreプロパティのgetter / settter定義

YourScoreプロパティにgetter/setterを定義しました。
setterでは、値が変更されていた場合、EventCallbackを呼び出します(YourScoreChanged.InvokeAsync(this._yourScore);)。

4.3. 実行

dotnet runで実行してみます。
ChildComponentのテキストボックス値を変更しフォーカスを移動すると、親コンポーネント(Index.razor)のYourScore値も変更されました。

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

4.4. ILを確認

Index.razor / ChildComponent.razorにいくつかの変更を加えましたがコンパイル後のILを確認すると、動作の繋がり・フローがよく分かります。
「bind_ex_blazor\bin\Debug\netstandard2.0\bind_ex_blazor.dll」をIlSpyでオープンします。

(1) ChildComponent.razor

まず、ChildComponentですが「YourScoreプロパティの定義」「BuildRenderTree()メソッドの定義」は以下のようになっています。

// bind_ex_blazor.Components.ChildComponent
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;

public class ChildComponent : ComponentBase
{
    private int _yourScore = 0;

    [Parameter]
    public EventCallback<int> YourScoreChanged
    {
        get;
        set;
    }

    [Parameter]
    public int YourScore
    {
        get
        {
            return _yourScore;
        }
        set
        {
            if (_yourScore != value)
            {
                _yourScore = value;
                YourScoreChanged.InvokeAsync(_yourScore);
            }
        }
    }

    protected override void BuildRenderTree(RenderTreeBuilder __builder)
    {
        __builder.OpenElement(0, "div");
        __builder.AddAttribute(1, "class", "container bg-secondary");
        __builder.AddMarkupContent(2, "\r\n  ");
        __builder.AddMarkupContent(3, "<h2>Child Component</h2>\r\n\r\n  ");
        __builder.OpenElement(4, "p");
        __builder.AddContent(5, "YourScore: ");
        __builder.AddContent(6, YourScore);
        __builder.CloseElement();
        __builder.AddMarkupContent(7, "\r\n  ");
        __builder.OpenElement(8, "input");
        __builder.AddAttribute(9, "type", "text");
        __builder.AddAttribute(10, "value", BindConverter.FormatValue(YourScore));
        __builder.AddAttribute(11, "onchange", EventCallback.Factory.CreateBinder(this, delegate(int __value)
        {
            YourScore = __value;
        }, YourScore));
        __builder.SetUpdatesAttributeName("value");
        __builder.CloseElement();
        __builder.AddMarkupContent(12, "\r\n");
        __builder.CloseElement();
    }
}

YourScoreプロパティの定義の方は、ほぼ元の実装の通りです。繰り返しですが、setterで値が変更されたらYourScoreChangedがInvokeされます。
BuildRenderTree()メソッドは、テキストボックスに対してonchangeがハンドルされYourScoreプロパティに代入が行われています。
「テキストボックス入力→onchange→YourScore setter呼び出し→YourScoreChangedのinvoke」という流れになります。

(2) 親コンポーネント(Index.razor)

次に親コンポーネントIndex.razorのBuildRenderTree()メソッドを見ていきます。

// bind_ex_blazor.Pages.Index
...
protected override void BuildRenderTree(RenderTreeBuilder __builder)
{
    __builder.OpenElement(0, "div");
    __builder.AddAttribute(1, "class", "container bg-primary");
    __builder.AddMarkupContent(2, "\r\n  ");
    __builder.AddMarkupContent(3, "<h2>Index Component</h2>\r\n  Score: ");
    __builder.AddContent(4, YourScore);
    __builder.AddMarkupContent(5, "\r\n  <br>\r\n  ");
    __builder.OpenElement(6, "button");
    __builder.AddAttribute(7, "onclick", EventCallback.Factory.Create<MouseEventArgs>((object)this, (Action)InclementScore));
    __builder.AddContent(8, "+");
    __builder.CloseElement();
    __builder.AddMarkupContent(9, "\r\n  ");
    __builder.OpenElement(10, "button");
    __builder.AddAttribute(11, "onclick", EventCallback.Factory.Create<MouseEventArgs>((object)this, (Action)DeclementScore));
    __builder.AddContent(12, "-");
    __builder.CloseElement();
    __builder.AddMarkupContent(13, "\r\n  <br>\r\n  ");
    __builder.OpenComponent<ChildComponent>(14);
    __builder.AddAttribute(15, "YourScore", RuntimeHelpers.TypeCheck(YourScore));
    __builder.AddAttribute(16, "YourScoreChanged", RuntimeHelpers.TypeCheck(EventCallback.Factory.Create(this, RuntimeHelpers.CreateInferredEventCallback(this, delegate(int __value)
    {
        YourScore = __value;
    }, YourScore))));
    __builder.CloseComponent();
    __builder.AddMarkupContent(17, "\r\n");
    __builder.CloseElement();
}

ChildComponentに対して「YourScoreChanged」イベントハンドラが定義されEventCallbackで受け取ったint値を自分のYourScoreに代入しています。
この「YourScoreChanged」実装は、Index.razorを以下の実装にした場合には出力されません。

<ChildComponent YourScore="@YourScore" />

今回の修正で以下のように実装したために追加された処理になります。

<ChildComponent @bind-YourScore="@YourScore" />

Blazorでは、「@bind-【プロパティ名】」と定義することで、「【プロパティ名】Changed」というEventCallbackを受け取る仕組みになっています。

という事で、整理すると以下のフローになります。

子コンポーネント側:
①テキストボックス入力
②onchange
③YourScore setter呼び出し
④YourScoreChangedのinvoke
↓↓↓  
親コンポーネント側:
⑤YourScoreChangedをハンドルして自らのYourScoreに変更値を反映
⑥UIにも変更が反映  

※以下の「bind-YourScore-YourScoreChanged」に関してはPreview6で廃止されました。

5. bind-YourScore-YourScoreChanged="@YourScore" によるデータバインド

前のサンプルで「bind-YourScore="@YourScore"」という定義を行いましたが、これは「bind-YourScore-YourScoreChanged="@YourScore"」と同意になります。
では、以下のように実装したらどうなるか?

# Index.razor #

<ChildComponent bind-YourScore-YourScoreChangeeeeeeeed="@YourScore" />

コンパイル後のILは以下のようになります。
定義した「YourScoreChangeeeeeeeed」がそのままEventCallback名としてコンパイルされました。

protected override void BuildRenderTree(RenderTreeBuilder builder)
{
  ...省略
  builder.OpenComponent<ChildComponent>(14);
    builder.AddAttribute(15, "YourScore", RuntimeHelpers.TypeCheck(BindMethods.GetValue(YourScore)));
    builder.AddAttribute(16, "YourScoreChangeeeeeeeed", EventCallback.Factory.CreateBinder(this, delegate(int __value)
  ...省略
}

これを正常に動作させる場合、ChildComponent側のEventCallback定義名も合わせて変更する必要があります。

# ChildComponent.razor #
  [Parameter]
  public EventCallback<int>YourScoreChangeeeeeeeed { get; set; }

3. まとめ

データバインディングの動作の仕組みをILレベルで確認してみました。
データバインド構文的には以下のような感じで、構文とその動きを理解すればいいわけですが、ILを見てみるとロジックとして動作が繋がってすっきりしました。

one-way: [property name]="@Value"
two-way: @bind-[property name]="@value"
bind-[property name]-[event callback] ※Preview6で廃止されました。

次は ryuichi111std.hatenablog.com

BlazorでSPAするぞ!(3) - パラメータ - 正式版対応済

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

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

ryuichi111std.hatenablog.com

前回 Component の基本的な作り方・使い方を見てみましたが、今回はComponentのパラメータ機能を見ていきます。

1. パラメータ(Parameter)の利用

コンポーネントは、コンポーネント(ページ等)内にネストして配置することができました。
そして以下のように タグ で配置しました。
以下は、PersonViewerコンポーネントを .razor に配置しています(PersonViewerはPerson情報を表示するコンポーネントを想定)。

<PersonViewer />

コンポーネントではタグ属性を利用して、親(コンテナとなっているコンポーネント)から子(配置されているコンポーネント)へパラメータを渡すことができます。
以下ではPersonViewerコンポーネントに対して「ID / FirstName / LastName」の3つのパラメータを渡しています。

<PersonViewer1
  ID="@CurrentPerson.ID"
  FirstName="@CurrentPerson.FirstName"
  LastName="@CurrentPerson.LastName" />

1.1. サンプル実装

では具体的なサンプル実装を。
プロジェクトを作成します。

dotnet new blazorwasm -o component_param_blazor

(1) モデルクラスを作成

Personモデルクラスを作成します(Models/Person.cs)。

# Models/Person.cs #

using System;

namespace component_param_blazor.Models {
  public class Person {
    public int ID { get; set; }
    public string FirstName{ get; set; }
    public string LastName{ get; set; }
  }
}

(2) コンポーネントを作成

PersonViewerコンポーネントを作成します(Components/PersonViewer1.razor)。

# Components/PersonViewer1.razor #

@using component_param_blazor.Models

<div class="container">
  <div class="row">
    <div class="col-sm">ID:</div>
    <div class="col-sm">@ID</div>
  </div>
  <div class="row">
    <div class="col-sm">FirstName:</div>
    <div class="col-sm">@FirstName</div>
  </div>
  <div class="row">
    <div class="col-sm">LastName:</div>
    <div class="col-sm">@LastName</div>
  </div>
</div>

@code {
  [Parameter]
  public int ID { get; set; }
  
  [Parameter]
  public string FirstName{ get; set; }

  [Parameter]
  public string LastName{ get; set; }
}
[Parameter]

コンポーネントがらパラメータとして受け取るプロパティには [Parameter] 属性を付けます。
コンポーネントからの受取ですが、アクセス修飾子はprivateでもOKです。 (← Preview版の時はprivateで行けてた気がする)

(3) PersonViewerコンポーネントをページに配置

Pages/Index.razorにPersonViewer1コンポーネントを配置します。
Index.razorのコードブロックで Personオブジェクト を作成して、PersonViewer1コンポーネントにパラメータとして各価を引き渡します。

# Pages/Index.razor #

@page "/"
@using component_param_blazor.Components
@using component_param_blazor.Models

<PersonViewer1
  ID="@CurrentPerson.ID"
  FirstName="@CurrentPerson.FirstName"
  LastName="@CurrentPerson.LastName" />

@code {
  private Person CurrentPerson { get; set; }

  protected override void OnInitialized() {
    this.CurrentPerson = new Person() {
      ID = 0,
      FirstName = "takashi",
      LastName = "sakata" } ;
  }
}

実行画面は以下の通りです。

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

1.2. サンプル実装(オブジェクトとしてパラメータを渡す)

[Parameter]属性で引き渡すパラメータは、リテラル型だけでなく任意のオブジェクト型も指定可能です。
先程の PersonViewer1.razor を Personオブジェクト を受け取るように修正した版(PersonViewer2.razor)が以下です。

# Components/PersonViewer2.razor #

@using component_param_blazor.Models

<div class="container">
  <div class="row">
    <div class="col-sm">ID:</div>
    <div class="col-sm">@Person.ID</div>
  </div>
  <div class="row">
    <div class="col-sm">FirstName:</div>
    <div class="col-sm">@Person.FirstName</div>
  </div>
  <div class="row">
    <div class="col-sm">LastName:</div>
    <div class="col-sm">@Person.LastName</div>
  </div>
</div>

@code {
  [Parameter]
  public Person Person { get; set; }
}
# Pages/Index.razor #

@page "/"
@using component_param_blazor.Components
@using component_param_blazor.Models

<PersonViewer2 Person="@CurrentPerson" />

@code {
  private Person CurrentPerson { get; set; }

  protected override void OnInitialized() {
    this.CurrentPerson = new Person() {
      ID = 0,
      FirstName = "takashi",
      LastName = "sakata" } ;
  }
}

2. Cascading Parameter

親子間のプロパティの受け渡しは、前述の通りコンポーネント定義にプロパティ設定を行えば可能です。
親子関係の階層が深くなった場合に、数珠繋ぎが煩雑になることに対する解決策の1つが Cascading Parameter です。

2.1. 単純なパラメータの引き回し

まず、親から子にパラメータを引き渡す前述の方法を使った場合の、パラメータの引き回し例です。
子の配下に孫コンポーネントがあり、パラメータオブジェクトをそのまま引き継ぐ必要がある場合、↓↓↓のように実装すれば可能です。

# Pages/Index.razor #

@page "/"
@using cascading_parameter_blazor.Components

<ChildComponent YourName="@YourName" />

@code {
  private string YourName = "ryuichi111std";
}
# Components/ChildComponent.razor (子コンポーネント)#

this is Child.
<br />
@YourName
<br />
<GrandChildComponent YourName="@YourName" />

@code {
  [Parameter]
  public string YourName { get; set; }
}
# Components/GrandChildComponent.razor (個コンポーネントに配置される 孫コンポーネント)#

this is GrandChild.
<br />
@YourName

@code {
  [Parameter]
  public string YourName { get; set; }
}

2.2. Cascadingパラメータによる引き回し

が、面倒なので、Cascading Parameter機能を使えば、子孫までパラメータを一括で引き回せます。
以下が実装サンプル。

(1) 親コンポーネントの実装

# Pages/Index.razor #

@page "/"
@using component_param_blazor.Components

<div class="container bg-primary">
  <CascadingValue Value="YourName" Name="YourNameParam">
    <ChildComponent />
  </CascadingValue>
</div>

@code {
  private string YourName = "ryuichi111std";
}
【CascadingValue】

パラメータを提供するコンポーネントで「<CascadingValue>要素」を定義し、その子要素としてパラメータを引き継ぎたいコンポーネントを配置します。
Value属性値はパラメータの値。@code {} コードブロックで定義した YourName 値を渡しています。
Name属性値はパラメータ名。子孫コンポーネントValue属性で指定した@YourNameの値を YourNameParam というパラメータとして参照することができます。

(2) 子コンポーネントの実装

# Components/ChildComponent.razor #

<div class="container bg-secondary">
  this is Child.
  <br />
  @YourName
  <br />
  <GrandChildComponent />
</div>

@code {
  [CascadingParameter (Name = "YourNameParam")]
  private string YourName { get; set; }
}

【CascadingParameter属性】

プロパティ定義に対して「CascadingParameter属性」を付与します。
親要素で定義されたCascadingValueの中からYourNameParamという名前のパラメータを、自コンポーネントのYourNameプロパティの値として取得する、という意味になります。

(3) 孫コンポーネントの実装

# Components/GrandChildComponent.razor #

<div class="container bg-success">
  this is GrandChild.
  <br />
  @YourName
</div>

@code {
  [CascadingParameter (Name = "YourNameParam")]
  private string YourName { get; set; }
}

こちらも同様に「CascadingParameter」属性をプロパティに付与することでパラメータを引き継ぐことができます。

実行画面は以下の通りです。

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

3. まとめ

ということでComponentのパラメータでした。
(ぜんぜん、まとめじゃない・・・)

次は ryuichi111std.hatenablog.com

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

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

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

ryuichi111std.hatenablog.com

BlazorでのUI要素であるComponent(コンポーネント)について見ていこうと思います。

1. コンポーネント(Component)

前回 Hello Blazor ページを実装しましたが、これ自体がコンポーネントです。
ページやダイアログ、ページ内の要素などのUI要素(オブジェクト)がコンポーネントです。
HTML/CSS と イベント処理などを行うロジックコード で構成されます。
コンポーネントは、ネストしてコンポーネント内に配置することも可能です。

まあ、ASP.NETとかWPFとかでも同様な一般的な UIコンポーネント とか コントロール とか言われるようなものですね。

1.1. SimpleHelloコンポーネントの作成(.razor)

コンポーネントは .razor 拡張子で作成します (Previewリリースより前は .cshtml 拡張子でした)
では超シンプルなコンポーネントを作成し、そのコンポーネントをページに配置したいと思います。

まずは dotnet new でプロジェクトを作成します。
名前はsimple_component_blazorとしました。

C:\workspace>dotnet new blazorwasm -o simple_component_blazor
The template "Blazor WebAssembly App" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on C:\workspace\simple_component_blazor\simple_component_blazor.csproj...
  復元対象のプロジェクトを決定しています...
  C:\workspace\simple_component_blazor\simple_component_blazor.csproj を復元しました (192 ms)。

Restore succeeded.

(1) SimpleHello.razorを作成

simple_component_blazorフォルダ直下に Componentsフォルダ を作成します。
そして、Components/SimpleHello.razor ファイルを作成します。

# Components/SimpleHello.razor #

@Message

@code {
  private string Message = "Hello シンプル過ぎるコンポーネント";
}
@Message と Message定義

「@Message」を記述することにより、@codeブロックで定義した変数Messageが画面上に表示されいます。

(2) SimpleHelloコンポーネントをページに配置

次にページ(Pages/Index.razorn)に、SimpleHelloコンポーネントを配置します。

# Pages/Index.razor #

@page "/"
@using simple_component_blazor.Components

<SimpleHello />
「@using」

まず「@using」。
Blazorの.razorでは、プロジェクト名+フォルダ階層が名前空間になります(※1)。
Index.razorは、simple_component_blazr.Pages名前空間のIndexクラスになります。
SimpleHello.razorは、simple_component_blazor.Components名前空間のSimpleHelloクラスになります。
Pages/Index.razorからは、別の名前空間コンポーネントを使うことになるので「@using simple_component_blazor.Components」として名前空間への参照を指定しておきます。

※1 namespaceを明示的に指定することも可能です。

@namespace hogehoge.ugougo

@Message

@code {
  private string Message = "Hello シンプル過ぎるコンポーネント";
}
「<SimpleHello />」

@usingでsimple_component_blazorへの参照を定義済みなので、コンポーネントコンポーネントクラス名(= 拡張子.razorを除いたファイル名 = SimpleHello)をタグで記述することで配置可能です。

※ using指定せず名前空間コンポーネント名をフルに記述してコンポーネントを配置することも可能

@page "/"

<simple_component_blazor.Components.SimpleHello />
「実行画面」

dotnet runで実行した画面は以下です。

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

1.2. Calculatorコンポーネントの作成(.razor)

次にCalculatorコンポーネントというものを作成したいと思います。
完成画面は以下です。
入力テキストボックスを2つ配置し、その和をreadonlyのテキストボックスに表示します。

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

まずは dotnet new でプロジェクトを作成します。
名前はcalc_blazorとしました。

C:\workspace>dotnet new blazorwasm -o calc_blazor
The template "Blazor WebAssembly App" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on C:\workspace\calc_blazor\calc_blazor.csproj...
  復元対象のプロジェクトを決定しています...
  C:\workspace\calc_blazor\calc_blazor.csproj を復元しました (192 ms)。

Restore succeeded.

(1) Calculationコンポーネントを作成

calc_blazorフォルダ直下に Componentsフォルダ を作成します。
Components/Calculator.razor / Components/Calculator.razor.cs ファイルを作成します。
(UI定義の .razor と コードビハインドの .cs です)

# Components/Calculator.razor #

<input type="text" @bind="Value1" />
+
<input type="text" @bind="Value2" />
=
<input type="text" readonly="true" value="@Result" />
<br />
<input type="button" @onclick="Calc" value="計算" />
# Components/Calculator.razor.cs #

using System;

namespace calc_blazor.Components
{
  public partial class Calculator
  {
    public int Value1 { get; set; } = 0;
    
    public int Value2 { get; set; } = 0;

    public int Result { get; set; } = 0;

    public void Calc() {
      this.Result = this.Value1 + this.Value2;
    }
  }
}
「Calculatorクラス」

コンポーネントロジッククラスの実装です。(partial形式で実装しています。https://ryuichi111std.hatenablog.com/entry/2019/05/01/220242
Value1 / Value2 は2つの入力項目、Result は計算結果を表すプロパティです。
また Calc()メソッド は、Value1 と Value2 の和を Result に設定する計算メソッドです。

「Calculator.razor」

主にUIを定義したrazorファイルです。
Value1 と Value2 は <input> タグの @bind属性 に対してバインディング設定されています。
これはTwo-Wayバインディングになります。ロジック上でValue1/Value2の値が変更されるとHTML上でのinput-valueの値が変更されますし、逆に、HTML UI上からユーザによって値の入力が行われるとValue1/Value2の値が変更されます。
Result は <input> タグの value属性 に対してバインディング設定されています。
これはOne-Wayバインディングになります。ロジック上でResultの値が変更されるとHTML上でのinput-valueの値が変更されますが、逆に、HTML UI上からユーザによって行われた値の変更はResultには反映されません(ここではReadOnlyとすることで入力自体出来なくしていますが)。
「<input type="button" @onclick="Calc" value="計算" />」により、ボタンクリック時に計算メソッド Calc() が呼び出されるようにしています。
Two-WayバインディングでHTML上の入力からプロパティ this.Value1 / this.Value2 に反映した値を合計し、this.Result を更新しています。this.Resultの変更はOne-WayバインディングによりUIに反映されます。

(2) Calculationコンポーネントをページに配置

作成した Components/Calculator コンポーネントを、Pages/Index.razor に配置します。

# Pages/Index.razor #

@page "/"
@using calc_blazor.Components

<Calculator />

dotnet run コマンドを実行するとCalculatorコンポーネントが画面に表示されます。

(さらに)Calculator.razorにイベントを追加

もう1つ。Calculatorコンポーネントにイベントを追加してみたいと思います。
計算が完了した際に計算完了イベント(計算完了コールバック)を実装します。

Calculator.razorにイベント(コールバック)を追加

まず、Calculator.razor.csコードビハインドクラスにイベントを追加します。

# Components/Calculator.razor.cs #

using System;
using Microsoft.AspNetCore.Components;

namespace calc_blazor.Components
{
  public partial class Calculator
  {
    public int Value1 { get; set; } = 0;
    
    public int Value2 { get; set; } = 0;

    public int Result { get; set; } = 0;

    [Parameter]
    public EventCallback<int> ChangeCalcResult { get; set; }

    public void Calc() {
      this.Result = this.Value1 + this.Value2;
      ChangeCalcResult.InvokeAsync(this.Result);
    }
  }
}
【ChangeCalcResult】

「public EventCallback ChangeCalcResult { get; set; }」がイベントハンドラの受け口になります。
[parameter]属性は今後(別投稿)の説明で出てきますが、ここでは「呼び出し元(親)から受け取る値について付ける属性」と思っておいてください。

Index.razorでイベント(コールバック)を受け取る

次にIndex.razor側でイベント(コールバック)を受け取ります。
こちらは、とりあえずコードビハインドを使わずに実装してしまいます。

# Pages/Index.razor #

@page "/"
@using calc_blazor.Components

<Calculator ChangeCalcResult="OnChangeCalcResult" />
<br />
計算結果:@ReceivedResult

@code {
  private int ReceivedResult = 0;

  private void OnChangeCalcResult(int result) {
    this.ReceivedResult = result;
  }
}
【ChangeCalcResult="OnChangeCalcResult"】

Calculatorタグの属性ChangeCalcResultにイベント(コールバック)を受け取るメソッド名を記述します。
OnChangeCalcResult()メソッドは@codeブロック内で定義しています。

dotnet runでの実行画面は以下です。

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

1.3. one-wayバインディングとtwo-wayバインディング

引き続き、作成したcalc_blazorを使ってDataBindingに関するちょっとした実験をしてみます。

<input value={@xxx}>によるOne-Wayバインディングについて、razorコードを不適切にいじって動作を確認してみます。
以下のように、Value2に対するバインディングを 「@bind → value」 に変更します。

# Components/Calculator.razor(Value2をOne-wayバインディングに変更) #

<input type="text" @bind="Value1" />
+
<input type="text" value="@Value2" />
=
<input type="text" readonly="true" value="@Result" />
<br />
<input type="button" @onclick="Calc" value="計算" />

つまり、Value2 は「HTML UIからの変更がコードのValue2変数に反映されない」状態になりました。
実行画面は以下です。
「10 + 20 = 10」という事で、Value2の値は計算コード上では 0 となってしまいました(HTML inputの値が反映されなかった)。

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

(1) valueと@bindのIL(Intermediate Language)

Blazor(というか.NET)は、.razor/.csをコンパイルしてDLL(アセンブリ)に変換しています。つまりC#をILに変換しています。
valueと@bindの違いは、ILを覗くとしっくり感じ取ることができます。
Value2への「bind→valueの変更を元に戻して」ビルドしておきます(dotnet buildコマンド実行)。
「calc_blazor\bin\Debug\netstandard2.0\calc_blazor.dll」をIlSpyで逆コンパイルした画面が以下です。

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

calc_blazor.Components.Calculator#BuildRenderTree(RenderTreeBuilder)メソッドがポイントです。
.razorで記述されたHTMLタグおよびRazor構文が解釈され、コードに落とし込まれています。
calc_blazor.Components.Calculator.BuildRenderTree()メソッドのコードは以下です。

protected override void BuildRenderTree(RenderTreeBuilder __builder)
{
  __builder.OpenElement(0, "input");
  __builder.AddAttribute(1, "type", "text");
  __builder.AddAttribute(2, "value", BindConverter.FormatValue(Value1));
  __builder.AddAttribute(3, "onchange", EventCallback.Factory.CreateBinder(this, delegate(int __value)
  {
    Value1 = __value;
  }, Value1));
  __builder.SetUpdatesAttributeName("value");
  __builder.CloseElement();
  __builder.AddMarkupContent(4, "\r\n+\r\n");
  __builder.OpenElement(5, "input");
  __builder.AddAttribute(6, "type", "text");
  __builder.AddAttribute(7, "value", BindConverter.FormatValue(Value2));
  __builder.AddAttribute(8, "onchange", EventCallback.Factory.CreateBinder(this, delegate(int __value)
  {
    Value2 = __value;
  }, Value2));
  __builder.SetUpdatesAttributeName("value");
  __builder.CloseElement();
  __builder.AddMarkupContent(9, "\r\n=\r\n");
  __builder.OpenElement(10, "input");
  __builder.AddAttribute(11, "type", "text");
  __builder.AddAttribute(12, "readonly", "true");
  __builder.AddAttribute(13, "value", Result);
  __builder.CloseElement();
  __builder.AddMarkupContent(14, "\r\n<br>\r\n");
  __builder.OpenElement(15, "input");
  __builder.AddAttribute(16, "type", "button");
  __builder.AddAttribute(17, "onclick", EventCallback.Factory.Create<MouseEventArgs>((object)this, (Action)Calc));
  __builder.AddAttribute(18, "value", "計算");
  __builder.CloseElement();
}

眺めれば、大体 Value1を構築している箇所 / Value2を構築している箇所 / Resultを構築している箇所 が分かります。
@bind属性を利用した Value1 / Value2 に対しては「onchangeに対してEventCallBackが作成され、デリゲートメソッド内で変更された値(__value)を base.Value1(コードビハインドのValue1プロパティ)に設定する」処理が実装されています。
一方value属性を利用した Result に対しては「onchangeに対する実装」がありません。
このように、.razorにおける value="@xxx" と @bind="xxx" の定義の違いは、対象要素に対して UIからの変更をバインドプロパティに反映するonchangeイベントハンドラ を生成するかどうかの違いになってきます。

1.4. リストデータを表示するページ(コンポーネント)を作成

次はリストデータを表示するページを作成してみます。
TodoList表示ページとします。
以下のイメージです。

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

これまでコンポーネントという表現を使っていましたがページという表現をしました。
ページも「=コンポーネント」ですが、ここでは Pages/TodoList.razor というページ(コンポーネント)を追加して実装してみます。

以下のコマンドで todo_blazor プロジェクトを作成しておきます。

dotnet new blazorwasm -o todo_blazor

(1) モデルクラスを作成

Todoを表すモデルクラスを作成します。
todo_blazorフォルダ直下に Modelsフォルダ を作成します。
Models/Todo.csを追加します。

# Models/Todo.cs #
using System;

namespace todo_blazor.Models {
  public class Todo {
    public int ID { get; set; }

    public string Contents { get; set; }

    public bool IsCompleted { get; set; }
  }
}

普通のC#のPOCOなモデルクラスです。

(2) ページ(コンポーネント)を作成

UIを定義した Pages/TodoList.razor とそのコードビハインド Pages/TodoListBase.cs を追加します。

まず、Pages/TodoList.razor。

# Pages/TodoList.razor #

@page "/Todos/list"
@inherits todo_blazor.Pages.TodoListBase
@using todo_blazor.Models

@foreach( Todo todo in this.Todos) {
  @if(todo.IsCompleted) {
    <s>@todo.Contents</s>
  }
  else {
    @todo.Contents
  }
  <br />
}
【@page】

@page は、Blazorのルーティング設定です。
「【アプリルート】/Todos/list」が、このページを表すURIになります。

【@inherits】

TodoList.razorのコードビハインドクラスを設定しています。
todo_blazor.Pages.TodoListBaseクラスがコードビハインドクラスになります(ここでは partial形式 ではなく、ComponentBaseクラスを継承したTodoListBaseクラスをコードビハインドクラスとしています)。

【@using】

.razor内でのレンダリングにモデルクラスを利用する為、@using todo_blazor.Models を設定しています(C#のusingと同じです)。

レンダリングロジック】

@foreach / @if でTodoをレンダリングしています。
これは従来からのASP.NET / ASP.NET Core の Razorエンジンによるテンプレート記述と同じ方式で記述可能です。

次に、コードビハインドクラス Pages/TodoListBase.cs。

# Pages/TodoListBase.cs #

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;
using todo_blazor.Models;

namespace todo_blazor.Pages
{
  public class TodoListBase : ComponentBase
  {
    public List<Todo> Todos { get; } = new List<Todo>();
    
    protected override void OnInitialized() {
      this.Todos.Add( new Todo() { ID = 0, Contents = "C#やる", IsCompleted = true } );
      this.Todos.Add( new Todo() { ID = 1, Contents = "TypeScriptやる", IsCompleted = false } );
      this.Todos.Add( new Todo() { ID = 2, Contents = "Babelやる", IsCompleted = true } );
      this.Todos.Add( new Todo() { ID = 3, Contents = "Blazorやる", IsCompleted = false } );
      this.Todos.Add( new Todo() { ID = 4, Contents = "Goやる", IsCompleted = false } );
    }
  }
}
【Todosプロパティ】

TodosプロパティにTodoのリストを保持し、プロパティとして公開しています。

【OnInitialized()メソッド】

OnInitialized()メソッドでTodoListを初期化しています。
OnInitialized()メソッドは、Blazorコンポーネントのライフサイクルメソッドで、コンポーネントの初期化時に呼び出されます。
通常のアプリではWebAPI呼び出しなどでTodoリストを取得すると思いますが、ここではローカルで固定の5つのTodoを作成します。
Todosプロパティの値が TodoList.razor に適用されて画面にレンダリングされます。

(3) メニューにTodoListを追加

自動生成されたプロジェクトテンプレートでは、サイドメニューに「Home / Counter / Fetch data」の3つが並んでいます。
TodoList.razor へのメニューリンクを追加します。
Shared/NavMenu.razor を開き、<li>要素の並びに以下を追加します。

# Shared/NavMenu.razor #
...省略...

<li class="nav-item px-3">
  <NavLink class="nav-link" href="Todos/list">
    <span class="oi oi-list-rich" aria-hidden="true"></span> Todo List
  </NavLink>
</li>

...省略...

NavLink要素の href属性 がページのURIを表します。ページ側で @page で指定したルーティングとマッチしたページにリンクするようになります。

以上で完成です。
コマンドプロンプトで(プロジェクトフォルダで) donet run コマンドにより実行することができます。

2. まとめ

という事でComponent(コンポーネント)についてのまとめ。
BlazorのUI要素は .razor拡張子 で定義し、これらがComponentと呼ばれるものである。
ページやコントロール的なもの含めてComponent(コンポーネント) と呼ぶ。
Component内には、Componentをネストして配置することができる。
Componentは、プロパティやイベントを持つことができる。

次は ryuichi111std.hatenablog.com