Quantcast
Channel: かずきのBlog@hatena
Viewing all articles
Browse latest Browse all 1387

ReactiveProperty で二度押し防止 2019 年 6 月版

$
0
0

ということで書いていきましょう。

といっても二度押し防止系は AsyncReactiveCommand使うと楽。以上です。 例えば非同期処理が終わるまで押せないボタンを実現したい場合は以下のような ViewModel になります。

using Reactive.Bindings;
using System.ComponentModel;
using System.Threading.Tasks;

namespace DoubleClickApp
{
    publicclass MainWindowViewModel : INotifyPropertyChanged
    {
        publicevent PropertyChangedEventHandler PropertyChanged;

        public AsyncReactiveCommand HeavyProcessCommand { get; }

        public ReactivePropertySlim<string> Message { get; }

        public MainWindowViewModel()
        {
            Message = new ReactivePropertySlim<string>();
            HeavyProcessCommand = new AsyncReactiveCommand()
                .WithSubscribe(HeavyProcessAsync);
        }

        private async Task HeavyProcessAsync()
        {
            Message.Value = "処理開始!!";
            await Task.Delay(3000);
            Message.Value = "処理終了!!";
        }
    }
}

XAML 側はこんな感じで普通に適当なボタンなどの Command プロパティに紐づけるだけです。

<Window x:Class="DoubleClickApp.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:local="clr-namespace:DoubleClickApp"mc:Ignorable="d"Title="MainWindow"Height="450"Width="800"><Window.DataContext><local:MainWindowViewModel /></Window.DataContext><StackPanel><TextBlock Text="{Binding Message.Value}" /><Button Content="Click me!!"Command="{Binding HeavyProcessCommand}" /></StackPanel></Window>

実行すると、いい感じに処理中はボタン押せなくなります。

f:id:okazuki:20190620114832g:plain

複数個非同期処理があって、どれかが実行中は他のボタンを押せなくしたいんだ

世の中はシンプルじゃなくて複数の非同期処理があって、どれかが実行中はボタンが押せないようにしたいということはよくあります。 AsyncReactiveCommandは、IReactiveProperty<bool>から生成することもできるのですが、この方法で作った AsyncReactiveCommandは押せない状態を共有します。

つまり、以下のように同じ IReactiveProperty<bool>を元に生成した AsyncReactiveCommand

using Reactive.Bindings;
using System.ComponentModel;
using System.Threading.Tasks;

namespace DoubleClickApp
{
    publicclass MainWindowViewModel : INotifyPropertyChanged
    {
        publicevent PropertyChangedEventHandler PropertyChanged;

        private ReactivePropertySlim<bool> SharedStatus { get; }

        public AsyncReactiveCommand HeavyProcessCommand1 { get; }
        public AsyncReactiveCommand HeavyProcessCommand2 { get; }
        public AsyncReactiveCommand HeavyProcessCommand3 { get; }

        public ReactivePropertySlim<string> Message { get; }

        public MainWindowViewModel()
        {
            Message = new ReactivePropertySlim<string>();

            SharedStatus = new ReactivePropertySlim<bool>(true); // 初期状態は実行可能で
            HeavyProcessCommand1 = SharedStatus
                .ToAsyncReactiveCommand()
                .WithSubscribe(HeavyProcess1Async);
            HeavyProcessCommand2 = SharedStatus
                .ToAsyncReactiveCommand()
                .WithSubscribe(HeavyProcess2Async);
            HeavyProcessCommand3 = SharedStatus
                .ToAsyncReactiveCommand()
                .WithSubscribe(HeavyProcess3Async);
        }

        private async Task HeavyProcess1Async()
        {
            Message.Value = "1 番開始!";
            await Task.Delay(3000);
            Message.Value = "1 番終了!!";
        }
        private async Task HeavyProcess2Async()
        {
            Message.Value = "2 番開始!";
            await Task.Delay(3000);
            Message.Value = "2 番終了!!";
        }
        private async Task HeavyProcess3Async()
        {
            Message.Value = "3 番開始!";
            await Task.Delay(3000);
            Message.Value = "3 番終了!!";
        }
    }
}

以下のように各々のボタンにバインドすると

<Window x:Class="DoubleClickApp.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:local="clr-namespace:DoubleClickApp"mc:Ignorable="d"Title="MainWindow"Height="450"Width="800"><Window.DataContext><local:MainWindowViewModel /></Window.DataContext><StackPanel><TextBlock Text="{Binding Message.Value}" /><Button Content="One"Command="{Binding HeavyProcessCommand1}" /><Button Content="Two"Command="{Binding HeavyProcessCommand2}" /><Button Content="Three"Command="{Binding HeavyProcessCommand3}" /></StackPanel></Window>

以下のようになります。

f:id:okazuki:20190620115915g:plain

やったね!

ギブアップ

入力エラーが無くなったら押せるボタンが複数あるんだけど、それの二度押し防止をしつつボタンは同時に1つしか押せないようにたいんだよね。

入力エラーの有無は IObservable<bool>で簡単に取れるので、それを ToReactivePropertyして、その ReactiveProperty<bool>から AsyncReactiveCommandを作れば勝つる!!と思うけど、それをやると以下のケースで死にます。

  • 入力エラーを無くす
  • どれかボタンを押す
  • 非同期処理が走ってる間に入力項目をエラーにして、再度エラーを無くす
  • 非同期処理が終わってなくてもボタンが押せるようになる!!

コードとしてはこんなイメージです。ダメな例。

using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;

namespace DoubleClickApp
{
    publicclass MainWindowViewModel : INotifyPropertyChanged
    {
        publicevent PropertyChangedEventHandler PropertyChanged;

        private ReactiveProperty<bool> SharedStatus { get; }

        public AsyncReactiveCommand HeavyProcessCommand { get; }

        public ReactivePropertySlim<string> Message { get; }

        [Required]
        public ReactiveProperty<string> Input { get; }

        public MainWindowViewModel()
        {
            Message = new ReactivePropertySlim<string>();

            Input = new ReactiveProperty<string>()
                .SetValidateAttribute(() => Input);

            SharedStatus = Input
                .ObserveHasErrors
                .Inverse()
                .ToReactiveProperty();
            HeavyProcessCommand = SharedStatus
                .ToAsyncReactiveCommand()
                .WithSubscribe(HeavyProcessAsync);
        }

        private async Task HeavyProcessAsync()
        {
            Message.Value = "開始!";
            await Task.Delay(3000);
            Message.Value = "終了!!";
        }
    }
}

上記コードはコマンドは一つですけど、まぁ同じダメなことが起こります。ReactiveProperty<bool>が外部から書き換えられてしまうのが問題ですね。あくまで AsyncReactiveCommand間でのステートの共有用にとどめたほうが問題が起きないです。

因みにボタンが 1 つのみの場合は以下のように普通に行けるので安心してください。

using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;

namespace DoubleClickApp
{
    publicclass MainWindowViewModel : INotifyPropertyChanged
    {
        publicevent PropertyChangedEventHandler PropertyChanged;

        public AsyncReactiveCommand HeavyProcessCommand { get; }

        public ReactivePropertySlim<string> Message { get; }

        [Required]
        public ReactiveProperty<string> Input { get; }

        public MainWindowViewModel()
        {
            Message = new ReactivePropertySlim<string>();

            Input = new ReactiveProperty<string>()
                .SetValidateAttribute(() => Input);

            // エラーが無くなったら押せる重たい処理のコマンド
            HeavyProcessCommand = Input.ObserveHasErrors
                .Inverse()
                .ToAsyncReactiveCommand()
                .WithSubscribe(HeavyProcessAsync);
        }

        private async Task HeavyProcessAsync()
        {
            Message.Value = "開始!";
            await Task.Delay(3000);
            Message.Value = "終了!!";
        }
    }
}

まとめ

AsyncReactiveCommand割と便利。


Viewing all articles
Browse latest Browse all 1387

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>