ReactivePropertyでプロパティが上手く書けた話


こんにちは。

boiler's Graphicsというベクターグラフィックスドローイングツールを開発していますが、その開発作業の中でReactivePropertyを使ったプロパティが上手く書けたのでそれを共有したいと思ってこの記事を書いています。はっきり言って、オ○ニー記事です。

ソースコードは掲載しますが、申し訳ありませんが、巨大なソースコードのため、各自で頑張って探してみてください。掲載するファイルのパスは載せますので。

線分クラスのLeftTopプロパティ

これはboiler's Graphicsで描画される線分の抽象クラスです。
このクラスに線分を囲う矩形の左上の点をLeftTopプロパティとして設けようと思います。
このクラスはPointsプロパティを持っています。例えば直線を定義する時、始点P0と終点P1で構成するとなると、PointsプロパティにはPoints = new ObservableCollection<Point>(); Points.Add(P0); Points.Add(P1);となります。

さて、Pointsプロパティには始点P0と終点P1が入ります。この時、即ち、P0とP1が追加される瞬間にLeftTopプロパティを更新したいと思います。やり方はかんたん。このクラスの初期化メソッド内で次のコードを書けばいいです。

LeftTop = Points.ObserveProperty(x => x.Count)
                            .Where(x => x > 0)
                            .Select(_ => new Point(Points.Min(x => x.X), Points.Min(x => x.Y)))
                            .ToReactiveProperty();

一行目、ObservePropertyではなんらかのプロパティを設定しなければいけませんが、Points(ObserveCollection)にはCountプロパティぐらいしかプロパティがないので、これを設定します。
二行目、Whereでは三行目のSelectでPointsプロパティに0個のPointしか追加されていなかった場合のことを考慮して、0個より多いという条件を設定します。これがないと、三行目のSelectで、Points.Min()してしまい、「シーケンスに要素がありません」と嘆かれて終了してしまいます。
三行目、SelectではPointsに含まれるPointの座標X、座標Yについて最小となるPointを生成して返します。これがLeftTopプロパティの実体(Value)となります。
四行目、ToReactiveProperty()と書いて終わりです。えっ、ToReadOnlyReactivePropertySlim()にはしないのかって?いえ、これで良いんです。なぜなら、このLeftTopプロパティは外部から変更される可能性があるためです。

ConnectorBaseViewModel.cs
    public abstract class ConnectorBaseViewModel : SelectableDesignerItemViewModelBase, IObserver<TransformNotification>, ICloneable
    {
        private ObservableCollection<Point> _Points;

        public ConnectorBaseViewModel(int id, IDiagramViewModel parent) : base(id, parent)
        {
            Init();
        }

        public ConnectorBaseViewModel()
        {
            Init();
        }

        public ReactiveProperty<Point> LeftTop { get; set; }
        :

        public ObservableCollection<Point> Points
        {
            get { return _Points; }
            set { SetProperty(ref _Points, value); }
        }

        private void Init()
        {
            _Points = new ObservableCollection<Point>();
            InitPathFinder();
            LeftTop = Points.ObserveProperty(x => x.Count)
                            .Where(x => x > 0)
                            .Select(_ => new Point(Points.Min(x => x.X), Points.Min(x => x.Y)))
                            .ToReactiveProperty();
            :
        }
        :
    }

線分クラスのWidth、Heightプロパティ

次はWidthプロパティとHeightプロパティです。

ConnectorBaseViewModel.cs
public abstract class ConnectorBaseViewModel : SelectableDesignerItemViewModelBase, IObserver<TransformNotification>, ICloneable
    {
    :
        public ReadOnlyReactivePropertySlim<double> Width { get; set; }

        public ReadOnlyReactivePropertySlim<double> Height { get; set; }
        :
        private void Init()
        {
            _Points = new ObservableCollection<Point>();
            InitPathFinder();
            LeftTop = Points.ObserveProperty(x => x.Count)
                            .Where(x => x > 0)
                            .Select(_ => new Point(Points.Min(x => x.X), Points.Min(x => x.Y)))
                            .ToReactiveProperty();
            Width = Points.ObserveProperty(x => x.Count)
                          .Where(x => x > 0)
                          .Select(_ => Points.Max(x => x.X) - Points.Min(x => x.X))
                          .ToReadOnlyReactivePropertySlim();
            Height = Points.ObserveProperty(x => x.Count)
                          .Where(x => x > 0)
                          .Select(_ => Points.Max(x => x.Y) - Points.Min(x => x.Y))
                          .ToReadOnlyReactivePropertySlim();
        }
        :
    }

大体やっていることは、LeftTopの時と同じです。特筆すべきは三行目でSelectをやっていますが、これは幅ならX座標の最大から最小を引いた値になります。高さならY座標の最大から最小を引いた値になります。あと、四行目はToReadOnlyReactivePropertySlim()になっています。こちらのプロパティは外部から変更される可能性はないので、参照オンリーなので、ReadOnlyにしています。

結び

いかがだったでしょうか。私はいつも思うのですが、ReactivePropertyはいざ書こうと思うと、難航してしまうのですが、コードを書けた時に大きな達成感を得られるので、私はReactivePropertyが好きです。
みなさんも、ReactivePropertyに苦戦しているという方はQiitaに記事を書くと良いと思います。きっと神様からコメントを貰えると思いますよ...。

ソースコード

コミット:3c39e15 あたり

今回使用したファイルはboilersGraphics/ViewModels/ConnectorBaseViewModel.csです。