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を期待しています^^