放浪軍師のアプリ開発局

Xamarin.Formsを使ってAndroid,iOS,UWP,WPFで動くアプリを開発したりしています。他にもいろいろやってます。尚、このブログはわからないところを頑張って解決するブログであるため、正しい保証がありませんのでご注意ください。

Blazor WebAssembly で ReactiveProperty 使って MVVM やってみる

どうも放浪軍師です。みなさんGWを楽しんでおられますでしょうか?私はもう育成育成育成しまくりで、マニーをたらふくため込んでいます。ただSPが枯渇してて辛いところですね。え?何の話だって?そりゃあもちろんGW(ゴルシウィーク)の事ですヨ。

Blazor WebAssembly で ReactiveProperty 使って MVVM

前回 Blazor WebAssembly の基本を押さえてみたわけですが、どうやら MVVM でも書けるらしいってんで挑戦してみました。もちろんみんな大好き ReactiveProperty も使っちゃいましょう!

環境

Microsoft Visual Studio Community 2019 Version 16.8.2
Microsoft.AspNetCore.Components.WebAssembly 3.2.1
ReactiveProperty 7.10.0

GitHub

参考にどうぞ
github.com

Extends

BindableBase.cs

INotifyPropertyChanged を継承したクラスです。これを最初に用意しておくと Model 層や ViewModel 層で楽できます。

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace BlazorTry.Extends {
    public class BindableBase : INotifyPropertyChanged {
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null) {
            if (Equals(field, value)) { return false; }
            field = value;
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); return true;
        }
    }
}

Models

Lottery.cs

Model 層です。今回もいつものように抽選してくれるクラスを用意しました。先ほど作った BindableBase クラスを継承してプロパティを簡単に書けるようにしています。機能としては、 Action() でリストから一つ抽選して Name プロパティに代入。 Add() で抽選リストに名前を追加といった感じです。MVVM なのでメソッドはすべて返り値がありません。

using BlazorTry.Extends;
using System;
using System.Collections.ObjectModel;

namespace BlazorTry.Models {
    public class Lottery : BindableBase {

        public string Name { get => name; set => SetProperty(ref name, value); }
        private string name;

        public ObservableCollection<string> NameList {
            get => nameList;
            set => SetProperty(ref nameList, value);
        }
        private ObservableCollection<string> nameList;

        public Lottery(ObservableCollection<string> nameList) {
            NameList = nameList;
            Name = "抽選結果表示";
        }

        public void Action() {
            if (NameList == null) return;
            var rnd = new Random((int)DateTime.Now.Ticks & 0x0000FFFF);
            Name = NameList[rnd.Next(0, NameList.Count)];
        }

        public void Add(string name) {
            NameList.Add(name);
        }
    }
}

ViewModels

LotteryViewModel.cs

ViewModel 層です。こちらも BindableBase を継承させています。コンストラクタの引数に Lottery を取り DI に備えつつ、あとはいつものように ReactiveProperty を使って簡潔に書きました。完全に Xamarin.Forms の時と同じノリですね。

using BlazorTry.Extends;
using BlazorTry.Models;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System;

namespace BlazorTry.ViewModels {
    public class LotteryPageViewModel : BindableBase {
        public ReadOnlyReactivePropertySlim<string> Name { get; }
        public ReactiveCommand Action { get; }
        public ReactiveCommand<string> AddParam { get; }
        public ReadOnlyReactiveCollection<string> NameList { get; }

        public ReactiveProperty<string> AddName { get; } = new ReactiveProperty<string>();

        public LotteryPageViewModel(Lottery lottery) {
            Name = lottery.ObserveProperty(x => x.Name).ToReadOnlyReactivePropertySlim();
            NameList = lottery.NameList.ToReadOnlyReactiveCollection();
            Action = new ReactiveCommand();
            Action.Subscribe(_ => lottery.Action());
            AddParam = new ReactiveCommand<string>();
            AddParam.Subscribe(x => lottery.Add(x));
        }
    }
}

Top

Program.cs

View 層にとりかかる前に View と ViewModel を DI コンテナに登録します。追加したのは以下。

  1. builder.Services.AddScoped(sp => new Lottery(nameList)); で Model を登録。
  2. builder.Services.AddTransient<LotteryPageViewModel>(); で ViewModel を登録。

Blazor には最初から DI コンテナが用意されているようですね。便利!

using BlazorTry.Models;
using BlazorTry.ViewModels;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.ObjectModel;
using System.Net.Http;
using System.Threading.Tasks;

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

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

            var nameList = new ObservableCollection<string> { "雨ヶ崎笑虹", "九条林檎", "白乃クロミ", "結目ユイ", "巻乃もなか" };
            builder.Services.AddScoped(sp => new Lottery(nameList));
            builder.Services.AddTransient<LotteryPageViewModel>();

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

Views

LotteryPage.razor

最後に View 層です。ここだけは Xamarin.Forms とはガラッと変わってきます。ポイントは以下のとおり。

  1. @using BlazorTry.ViewModels で ViewModels を呼べるようにする。
  2. @inject LotteryPageViewModel viewModel で DIコンテナから ViewModel を呼び出す。
  3. @viewModel.Name.Value で ViewModel の ReactiveProperty と繋ぐ。.Value を忘れぬよう。
  4. <button @onclick="()=>viewModel.Action.Execute()"> で ViewModel の ReactiveCommand と繋ぐ。
  5. <button @onclick='()=>viewModel.Action.Execute("name")'> で ViewModel の ReactiveCommand <string> と繋ぐ。Execute への引数がパラメータとなるが、その際シングルクォートで囲まないとダブルクォートが使えないので注意。

要所させ押さえてしまえば、 Xaml よりずっと書きやすい気がしますね。

@page "/lotterypage"
@using BlazorTry.ViewModels
@inject LotteryPageViewModel viewModel

<h3>ランダムで一人抽選するよ</h3>

@if (viewModel.NameList == null) {
    <p><em>Loading...</em></p>
} else {
    <table class="table">
        <thead>
            <tr>
                <th>抽選リスト</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var nameList in viewModel.NameList) {
                <tr>
                    <td>@nameList</td>
                </tr>
            }
        </tbody>
    </table>
}

<p><b>@viewModel.Name.Value</b></p>

<p><button class="btn btn-primary" @onclick="()=>viewModel.Action.Execute()">抽選!!!</button></p>
<p>
    <button class="btn btn-primary" @onclick='()=>viewModel.AddParam.Execute("夏狂乱")'>夏狂乱 追加</button>
    <button class="btn btn-primary" @onclick='()=>viewModel.AddParam.Execute("放浪軍師")'>放浪軍師 追加</button>
    <button class="btn btn-primary" @onclick='()=>viewModel.AddParam.Execute("漆原鎌足")'>漆原鎌足 追加</button>
</p>
<p>
    <input @bind="@viewModel.AddName.Value" />
    <button class="btn btn-primary" @onclick='()=>viewModel.AddParam.Execute(viewModel.AddName.Value)'>任意 追加</button>
</p>

動作確認

こんな感じになりました。

f:id:roamschemer:20210505041502g:plain:w450

まとめ

View 以外は Xamarin.Forms の時とまったく同じコードで行けてしまいましたね。驚きです。しかも View は Xaml より書きやすい感じがするし、これはもう Blazor WebAssembly 完全に理解したと言っても良いんじゃないでしょうか?さて、あとは Azure へのデプロイだな。