dnackのブログ

コード書く仕事の間にコード書いてる知命目前のおっさんです。

Prism/XamarinでAndroidアプリの作成(8):ReactivePropertyを使って排他処理

まあ、全然コード書く暇ないと言ってたのは嘘じゃなくて今も泣きながら仕事でコード書いているんだけど(?) 仕事のコード書いてる間にReactivePropertyの新しい使い方を覚えたので書いておく。 ReactivePropertyマジ便利。

今回のお題は3つ

1. ReactiveCommandを、ある条件で実行可能にする(i.e.ある条件で実行不可にする)

2. 1.の仕組みを使って、複数のコマンドを排他する。

3. あるコマンドが2の仕組みによってコマンド実行できなかった場合、できるようになったら実行するようにする。(これがやりたいこと)

をReactiveProperyとReactiveCommandの組わせてでむちゃ読みやすくかけるみたい。

以下は御託なので上のリンクからコードに飛んじゃってくれてOKです。

今回のこれの動機ですが、

あるメソッドのコールバックで画面遷移をしてしまう(ユーザ操作がタイミングに直接影響しない)画面で、

・ユーザがボタンをタップしたときに表示されるあるダイアログが表示表示されている間は画面遷移をしないで、

・そのダイアログが表示されている間に画面遷移の条件が満たされた場合はダイアログが破棄されたときに画面遷移する。

という処理をなんかわかりやすく書けないかというところ。(抽象化すると上の3つ目)

まあ、素直にセマフォ使えやって話なんだろうけど、なんか重たそうだし面倒くさそう。

コマンド実行時にCanExecuteが立たなかったらCanExecuteChangedを自分で購読するみたいなことができたらいいのかなあと思ったんだが、やり方がわからないしなんかそれはそれで危うそう。

(Prism/Xamarin なのか Android なのか、とにかくこの環境やりだしてから 「こうすればいいんだろうけど書き方がわからん(しそもそもこの環境でそれができるのかわからんしやっていいのかもわからん)」が多くていやになるな。勉強しろ。はい。

というわけでいきなり参考URLから

本家のドキュメントむっちゃ充実してきましたね。(まえからでした?)

okazuki.jp

英語はちょっと、、という私には同じくokazukiさんのこれなんかもすごい。

qiita.com

本当にこんなに有用なライブラリを書いた挙句、 私くらいの適当な理解でもとりあえず使えるくらいに 丁寧なドキュメントまで用意してくれるとは。 神か。 というわけでこれを読めばええように使えます。解散。

1. 実行条件付きのRactiveCommand

とあるBool値がtrueの時だけ押せるボタンを作りましょうみたいなのは基本中の基本。 ReactiveCommandはICommandを継承しているので、そのCanExecuteに条件を書けば使えるので、それ自体はなんてことない。 これを、とあるBool値をReactiveProperty(というかIObservable)にすると、こういう風に書けてしまうらしい。 神か。

        public ReactiveCommand ButtonACommand { get; }
        public ReactivePropertySlim<bool> Enable { get; }

        public MainPageViewModel()
        {
            Enable = new ReactivePropertySlim<bool>(true);

            ButtonACommand = Enable
                .ToReactiveCommand()
                .WithSubscribe(CommandAExecute);
        }

ReactivePropertyからToReactiveCommandすると、なんとそのboolがtrueの時だけ実行できるコマンドができる。 お手軽か。

なおコードではReactivePropertyではなくてReactivePropertySlimを使っているが、なるべくこっちを使おうね。むっちゃ軽いので。(といいつつ考えるの面倒だから全部ReactivePropertyにしてしまいがちなんだよなぁ…)

この辺は参考コードは本家とか見られるので、動く形のコードは省略(一応動かしたコードからの抜粋ではある)。 やっぱり神なんだよなぁこれ。。

2. 1.の仕組みを使って、複数のコマンドを排他する。

に行きたいんだけどその前に

ボタンの二度押し抑制

これも本家にコードがあるので動く例はそっちに譲るけど、 AsyncReactiveCommandというのがあって、上のReactiveCommandによる起動制御と合わせて次のように書くと、 コマンド発行した瞬間から処理が終わるまでEnableがfalseになって、2度押しの抑制ができる。

        public AsyncReactiveCommand ButtonACommand { get; }
        public ReactivePropertySlim<bool> Enable { get; }

        public MainPageViewModel()
        {
            Enable = new ReactivePropertySlim<bool>(true);

            ButtonACommand = Enable
                .ToAsyncReactiveCommand ()
                .WithSubscribe(CommandAExecuteAsync);
        }
        
        private async Task CommandAExecuteAsync()
        {
            await Task.Delay(3000);       //なんか重い処理
        }

でもって排他処理

Enableを共有したら本当に複数コマンドの排他処理できんじゃねーのって思うよね。 痒い所に手が届くReactivePropertyにそれができないわけがない。(マジで神)

このコードはPrismTemplateでつくったBlanAppで Viewに適当にボタンとテキスト張れば動くよ。(usingは付けてね)

namespace ReactiveCommandStudy.ViewModels
{

    public class MainPageViewModel : ViewModelBase
    {
        public AsyncReactiveCommand ButtonACommand { get; }
        public AsyncReactiveCommand ButtonBCommand { get; }
        public ReactivePropertySlim<bool> SharedEnable { get; }
        public ReactivePropertySlim<string> Message { get; }

        public MainPageViewModel(INavigationService navigationService)
            : base(navigationService)
        {
             SharedEnable = new ReactivePropertySlim<bool>(true);
            Message = new ReactivePropertySlim<string>();

            ButtonACommand = SharedEnable
                .ToAsyncReactiveCommand()
                .WithSubscribe(HeavyCommandAAsync);

            ButtonBCommand = SharedEnable
               .ToAsyncReactiveCommand()
               .WithSubscribe(HeavyCommandBAsync);
        }

        private async Task HeavyCommandAAsync()
        {
            Message.Value = "A Button";
            await Task.Delay(3000);
            Message.Value = "A Button Released";
        }
        private async Task HeavyCommandBAsync()
        {
            Message.Value = "B Button";
            await Task.Delay(3000);
            Message.Value = "B Button Released";
        }

    }
}

同じReactivePropertySlim<bool>からコマンドを作ると、うまいこと排他制御してくれるってわけ。 待ち行列のないセマフォみたい。なんだこれこんなに簡単にできるのか。なぜオレはあんなムダな時間を…(三井顔で)

3. 実行できなかったコマンドを実行可能になったら実行する。

待ち行列のない”セマフォといったが、やっぱり待ち行列が欲しいわけ。 待ちたい処理が1つだけなら行列作らなくてもこれでいける。 (2つ以上でも順番にこだわらないのならこれで行けるのかな?)

まずはコードから

View

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="ReactiveCommandStudy.Views.MainPage"
             Title="{Binding Title}">

    <StackLayout HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
        <Label Text="{Binding Message.Value}" />

        <Label Text="A:重い処理" />
        <Button Text="Button_A" Command="{Binding ButtonACommand}" />

        <Label Text="D:待ってから処理" />
        <Button Text="Button_D" Command="{Binding ButtonDCommand}" />

    </StackLayout>

</ContentPage>

ViewModel

using Prism.Navigation;
using Reactive.Bindings;
using System.Threading.Tasks;

namespace ReactiveCommandStudy.ViewModels
{

    public class MainPageViewModel : ViewModelBase
    {
        public AsyncReactiveCommand ButtonACommand { get; }
        public AsyncReactiveCommand ButtonDCommand { get; }
        public ReactivePropertySlim<bool> SharedStatus { get; }
        public ReactivePropertySlim<string> Message { get; }

        public ReactiveCommand OnTimerEvent { get; }


        public MainPageViewModel(INavigationService navigationService )
            : base(navigationService)
        {
            Title = "Main Page";
            SharedStatus = new ReactivePropertySlim<bool>(true);
            Message = new ReactivePropertySlim<string>();

            ButtonDCommand = new AsyncReactiveCommand()
                .WithSubscribe(CommandD);

            ButtonACommand = SharedStatus
                .ToAsyncReactiveCommand()
                .WithSubscribe(HeavyCommandAAsync);
        
        }

        private async Task HeavyCommandAAsync()
        {
            Message.Value = "A Button";
            await Task.Delay(3000);
            Message.Value = "A Button Released";
        }

        private async Task CommandD()
        {
            if (!SharedStatus.Value)
                await SharedStatus.WaitUntilValueChangedAsync<bool>();
            Message.Value = "D Button";
        }

    }
}

ボタンAを押している間にボタンDを押せるけど、実行はボタンAが終わってからになる。 (Aを押して、AボタンがInactiveな表示になっている間にDを押すと、 画面表示が"A Button Released"ではなくて"D Button"になっているので、 ちゃんとAがおわってからDが実行されたのがわかると思う。)

本家にはちゃんとCancellationToken使いなさいって書いてるんだけど、わかんないのでこれで! (おいおいお勉強しやす。すいません) まあ、というわけでとりあえず今日のところはこんな感じで。