LoginSignup
17
15

More than 3 years have passed since last update.

ReactiveProperty 定義するとコンストラクタが長くなる問題

Posted at

こういうやつです。

using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System;
using System.ComponentModel;
using System.Reactive.Disposables;
using System.Reactive.Linq;

namespace WpfApp2
{
    public class MainWindowViewModel : INotifyPropertyChanged, IDisposable
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private readonly CompositeDisposable _disposables = new CompositeDisposable();

        public ReactiveProperty<string> FirstName { get; }

        public ReactiveProperty<string> LastName { get; }
        public ReadOnlyReactivePropertySlim<string> FullName { get; }

        public MainWindowViewModel()
        {
            // 数が増えたりしてくると、ちょっと長くなっちゃう…
            FirstName = new ReactiveProperty<string>("").AddTo(_disposables);
            LastName = new ReactiveProperty<string>("").AddTo(_disposables);
            FullName = FirstName.CombineLatest(LastName, (f, l) => $"{f} {l}").ToReadOnlyReactivePropertySlim()
                .AddTo(_disposables);
        }

        public void Dispose() => _disposables.Dispose();
    }
}

この例では短いけど、増えてくるとね。

結論

これといった解決策はないですが、一応緩和する方法はあります。

緩和方法

モデルに書こうぜ

アプリのロジックが入ってるならモデルレイヤーに押しやれないか考えましょう。

メソッドに切り出そうぜ

ReactiveProperty の初期化を private メソッドにしようとするとプロパティを ReactiveProperty<string> Hoge { get; } から ReactiveProperty<string> Hoge { get; private set; } にしないといけなくて、もやっとする問題は、代入はコンストラクタでやって組み立てをメソッドにすることで緩和は出来ます。

using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System;
using System.ComponentModel;
using System.Reactive.Disposables;
using System.Reactive.Linq;

namespace WpfApp2
{
    public class MainWindowViewModel : INotifyPropertyChanged, IDisposable
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private readonly CompositeDisposable _disposables = new CompositeDisposable();

        public ReactiveProperty<string> FirstName { get; }

        public ReactiveProperty<string> LastName { get; }
        public ReadOnlyReactivePropertySlim<string> FullName { get; }

        public MainWindowViewModel()
        {
            FirstName = CreateFirstNameProperty();
            LastName = CreateLastNameProperty();
            FullName = CreateFullNameProperty();
        }

        // 本当はモデルとかと繋ぐ処理を書く
        private ReactiveProperty<string> CreateFirstNameProperty() =>
            new ReactiveProperty<string>("")
            .AddTo(_disposables); // 今回はいらないけど

        private ReactiveProperty<string> CreateLastNameProperty() =>
            new ReactiveProperty<string>("")
            .AddTo(_disposables); // 今回はいらないけど

        private ReadOnlyReactivePropertySlim<string> CreateFullNameProperty() =>
            FirstName.CombineLatest(LastName, (first, last) => $"{first} {last}")
                .ToReadOnlyReactivePropertySlim()
                .AddTo(_disposables); // 今回はいらないけど

        public void Dispose() => _disposables.Dispose();
    }
}

取り立てて目新しいことではないですが、一応メモです。

思いついただけ

こうも書けますね。

using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System;
using System.ComponentModel;
using System.Reactive.Disposables;
using System.Reactive.Linq;

namespace WpfApp2
{
    public class MainWindowViewModel : INotifyPropertyChanged, IDisposable
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private readonly CompositeDisposable _disposables = new CompositeDisposable();

        public ReactiveProperty<string> FirstName { get; }

        public ReactiveProperty<string> LastName { get; }
        public ReadOnlyReactivePropertySlim<string> FullName { get; }

        public MainWindowViewModel() =>
            (FirstName, LastName, FullName) = InitializeProperties();

        private (ReactiveProperty<string> firstName, ReactiveProperty<string> lastName, ReadOnlyReactivePropertySlim<string> fullName) InitializeProperties()
        {
            var firstName = new ReactiveProperty<string>("").AddTo(_disposables);
            var lastName = new ReactiveProperty<string>("").AddTo(_disposables);
            var fullName = firstName.CombineLatest(lastName, (f, l) => $"{f} {l}").ToReadOnlyReactivePropertySlim()
                .AddTo(_disposables);
            return (firstName, lastName, fullName);
        }


        public void Dispose() => _disposables.Dispose();
    }
}

何か今風っぽい感を出せるだけ。欠点としては、順番を間違えても気づきにくいところでしょうか。下のように。

using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System;
using System.ComponentModel;
using System.Reactive.Disposables;
using System.Reactive.Linq;

namespace WpfApp2
{
    public class MainWindowViewModel : INotifyPropertyChanged, IDisposable
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private readonly CompositeDisposable _disposables = new CompositeDisposable();

        public ReactiveProperty<string> FirstName { get; }

        public ReactiveProperty<string> LastName { get; }
        public ReadOnlyReactivePropertySlim<string> FullName { get; }

        public MainWindowViewModel() =>
            // FirstName「もしかして」LastName「私たち」FullName「入れ替わってる~!?」
            (LastName, FirstName, FullName) = InitializeProperties();

        private (ReactiveProperty<string> firstName, ReactiveProperty<string> lastName, ReadOnlyReactivePropertySlim<string> fullName) InitializeProperties()
        {
            var firstName = new ReactiveProperty<string>("").AddTo(_disposables);
            var lastName = new ReactiveProperty<string>("").AddTo(_disposables);
            var fullName = firstName.CombineLatest(lastName, (f, l) => $"{f} {l}").ToReadOnlyReactivePropertySlim()
                .AddTo(_disposables);
            return (firstName, lastName, fullName);
        }

        public void Dispose() => _disposables.Dispose();
    }
}

数が増えるとコンパイラーにも検知されない、見ててもわかりにくい間違いを生みそうなので、今回は普通のメソッド切り出しのほうがいいように感じてます。

NG パターン

プロパティに初期化を直接書けばいいじゃないか

以下のようなノリですね。

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

上記以上のことをしようとすると破綻します。ここでは他のフィールドやプロパティなどにはアクセスできないので、FullName を組み立てようとしたりすると詰みます。

じゃぁ => で書こう

毎回、違うインスタンスの ReactiveProperty が作られるのでやめたほうが良いケースが多いと思います。

例えば以下のようにやるとコンパイルは通りますが

// MainWindowViewModel.cs
public ReactiveProperty<string> FirstName => new ReactiveProperty<string>().AddTo(_disposable);
public ReactiveProperty<string> LastName => new ReactiveProperty<string>().AddTo(_disposable);
public ReadOnlyReactivePropertySlim<string> FullName => FirstName.CombineLatest(LastName, (f, l) => $"{f} {l}").ToReadOnlyReactivePropertySlim().AddTo(_disposable);

FirstName や LastName や FullName にアクセスするたびに新しいインスタンスになるので思った通りには動きません。

var vm = new MainWindowViewModel();
var f1 = vm.FirstName; // 1 つ新しい ReactiveProperty のインスタンスが作られる
var l1 = vm.LastName; // 1 つ新しい ReactiveProperty のインスタンスが作られる
var f2 = vm.FirstName; // 1 つ新しい ReactiveProperty のインスタンスが作られる
var l2 = vm.LastName; // 1 つ新しい ReactiveProperty のインスタンスが作られる

var fullName1 = vm.FullName; // 新しい FirstName と LastName 用の ReactiveProperty が別途作られて、それを加工した結果を格納する ReadOnlyReactivePropertySlim が作られる
var fullName2 = vm.FullName; // 新しい FirstName と LastName 用の ReactiveProperty が別途作られて、それを加工した結果を格納する ReadOnlyReactivePropertySlim が作られる

// 値を設定しても
f1.Value = "Kazuki";
l1.Value = "Ota";

// 別インスタンスなので当然同期されない
Console.WriteLine(f2.Value); // 空文字
Console.WriteLine(l2.Value); // 空文字

// FullName もそうね
Console.WriteLine(fullName1.Value); // 空白
Console.WriteLine(fullName2.Value); // 空白

// もちろん VM のもそう
Console.WriteLine(vm.FirstName.Value); // 空文字
Console.WriteLine(vm.LastName.Value); // 空文字
Console.WriteLine(vm.FullName.Value); // 空白

こう書けば、まだいけるケース

ちょっと初見殺し感あるかも。

using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System;
using System.ComponentModel;
using System.Reactive.Disposables;
using System.Reactive.Linq;

namespace WpfApp2
{
    public class MainWindowViewModel : INotifyPropertyChanged, IDisposable
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private readonly CompositeDisposable _disposables = new CompositeDisposable();

        private ReactiveProperty<string> _firstName;
        public ReactiveProperty<string> FirstName =>
            _firstName ?? (_firstName = new ReactiveProperty<string>("").AddTo(_disposables));

        private ReactiveProperty<string> _lastName;
        public ReactiveProperty<string> LastName =>
            _lastName ?? (_lastName = new ReactiveProperty<string>("").AddTo(_disposables));

        private ReadOnlyReactivePropertySlim<string> _fullName;
        public ReadOnlyReactivePropertySlim<string> FullName =>
            _fullName ?? (_fullName = FirstName
                .CombineLatest(LastName, (f, l) => $"{f} {l}")
                .ToReadOnlyReactivePropertySlim()
                .AddTo(_disposables));

        public void Dispose() => _disposables.Dispose();
    }
}

C# 8 からなら、これでいける

using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System;
using System.ComponentModel;
using System.Reactive.Disposables;
using System.Reactive.Linq;

namespace WpfApp2
{
    public class MainWindowViewModel : INotifyPropertyChanged, IDisposable
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private readonly CompositeDisposable _disposables = new CompositeDisposable();

        private ReactiveProperty<string> _firstName;
        public ReactiveProperty<string> FirstName =>
            _firstName ??= new ReactiveProperty<string>("").AddTo(_disposables);

        private ReactiveProperty<string> _lastName;
        public ReactiveProperty<string> LastName =>
            _lastName ??= new ReactiveProperty<string>("").AddTo(_disposables);

        private ReadOnlyReactivePropertySlim<string> _fullName;
        public ReadOnlyReactivePropertySlim<string> FullName =>
            _fullName ??= FirstName
                .CombineLatest(LastName, (f, l) => $"{f} {l}")
                .ToReadOnlyReactivePropertySlim()
                .AddTo(_disposables);

        public void Dispose() => _disposables.Dispose();
    }
}

これは割といいかも。コードスニペット作ろうかな。

まとめ

最後の書きかた気に入ったかも。

17
15
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
15