放浪軍師のアプリ開発局

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

Xamarin.Forms で Prism と ReactiveProperty で MVVM な自作コントロールを作りたい(2)

さて、始まりました放浪軍師のアプリ開発。今回は、去年のXamarin Advent Calendar記事でできなかった自作コントロールを貼り付けた親からの操作について書きたいと思いますので、よろしくお願いします。続き物となりますので、見てない方は前記事から見る事をお勧めいたします。

www.gunshi.info

自作コントロールを親からの ReactiveProperty で操作できないぞ…

前回の記事の終盤、結局自作コントロールを貼り付けた親からの ReactiveProperty を使った操作ができませんでした。この原因は MyContentView のプロパティ追加のやり方をミスったんだと思い込んでいたのですが、どうやら違ったようです。

開発環境

Visual Studio 2019 Version 16.4.0
Xamarin.Forms Version 4.4.0.991265
Prism.Dryloc.Forms Version 7.2.0.1422
ReactiveProperty 6.1.4

サンプルコード

今回のサンプルコードはGitHubにまとめてあります。よろしければ合わせてごらんください。

github.com

親ページの ViewModel とはそもそも繋がっていない?

前回の記事を公開した直後、かむ (@muak_x) | Twitterさんが以下のようなことを教えてくださいました。

つまり、【Prism の View と ViewModel を自動的に繋いでくれる機能】により自作コントロールの Binding 先が貼り付け元である親 Page の ViewModel ではなく、子の ViewModel になってしまっているから動かないよという事だそうです。…ってぇ、どういうこっちゃ!?…と中々に想像しにくかったので別プロジェクトを立ち上げて以下のような実験をしてみました。

動作実験プログラム

[RundomColor.cs]

using Prism.Mvvm;
using System;
using Xamarin.Forms;

namespace ContentViewTest.Models {

    /// <summary>
    /// ランダム色クラス
    /// </summary>
    public class RundomColor : BindableBase {

        /// <summary>
        /// 色
        /// </summary>
        public Color Color {
            get => color;
            private set => SetProperty(ref color, value);
        }
        private Color color;

        /// <summary>
        /// Colorにランダムで色をSetする
        /// </summary>
        public void SetColor() {
            int seed = Environment.TickCount;
            var rnd = new System.Random(seed);
            var r = rnd.Next(0, 255);
            var g = rnd.Next(0, 255);
            var b = rnd.Next(0, 255);
            Color = Color.FromRgb(r, g, b);
        }
    }
}

最初にModelです。これはSetColor()メソッドを実行するとColorをランダムに決定するだけの単純なクラスです。ランダムは私のプログラムでは基本ですね。

[MyControlViewModel.cs]

using ContentViewTest.Models;
using Prism.Mvvm;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System;
using System.Reactive.Disposables;
using Xamarin.Forms;

namespace ContentViewTest.ViewModels {
    public class MyControlViewModel : BindableBase {
        private CompositeDisposable Disposable { get; } = new CompositeDisposable();
        public RundomColor RundomColorModel = new RundomColor();
        public ReactiveProperty<Color> BackColor { get; set; } = new ReactiveProperty<Color>();
        public ReactiveCommand Command { get; } = new ReactiveCommand();

        public MyControlViewModel() {
            BackColor = RundomColorModel.ObserveProperty(x => x.Color)
                                        .ToReactiveProperty<Color>()
                                        .AddTo(this.Disposable);
            Command.Subscribe(_ => RundomColorModel.SetColor());
        }
        public void Dispose() => this.Disposable.Dispose();
    }
}

自作コントロールのViewModel です。Command が発火すると 前述の SetColor() が実行され、ReactiveProperty の効果で BackColor.Value が変化します。

[MyControl.xaml]

<?xml version="1.0" encoding="utf-8" ?>
<ContentView
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:prism="http://prismlibrary.com"
    prism:ViewModelLocator.AutowireViewModel="True"
    x:Class="ContentViewTest.Views.MyControl">

    <StackLayout>
        <Label Text="子"/>
        <Button Text="子から押す" Command="{Binding Command}"/>
    </StackLayout>

</ContentView>

自作コントロールの View です。Button の Command が発火すると ViewModel で BackColor.Value が変化しますが、View のどこにも {Binding BackColor.Value} が無い為、何も起きないと予想されます。

[MainPageViewModel.cs]

using ContentViewTest.Models;
using Prism.Navigation;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Xamarin.Forms;

namespace ContentViewTest.ViewModels {
    public class MainPageViewModel : ViewModelBase, IDisposable {
        private CompositeDisposable Disposable { get; } = new CompositeDisposable();
        public RundomColor RundomColorModel = new RundomColor();
        public ReactiveProperty<Color> BackColor { get; set; } = new ReactiveProperty<Color>();
        public ReactiveCommand Command { get; } = new ReactiveCommand();

        public MainPageViewModel(INavigationService navigationService)
            : base(navigationService) {
            BackColor = RundomColorModel.ObserveProperty(x => x.Color)
                                        .ToReactiveProperty<Color>()
                                        .AddTo(this.Disposable);
            Command.Subscribe(_ => RundomColorModel.SetColor());
        }
        public void Dispose() => this.Disposable.Dispose();
    }
}

親ページの ViewModel です。Command が発火すると 前述の SetColor() が実行され、ReactiveProperty の効果で BackColor.Value が変化する、先ほどの子の ViewModel とまったく構造になっています。

[MainPage.xaml]

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:Cont="clr-namespace:ContentViewTest.Views"
             xmlns:ViewModels="clr-namespace:ContentViewTest.ViewModels"
             x:Class="ContentViewTest.Views.MainPage"
             x:Name="self">

    <StackLayout>
        <Cont:MyControl BackgroundColor="{Binding BackColor.Value}"/>
        <StackLayout BackgroundColor="{Binding BackColor.Value}">
            <Label Text="親見本" />
            <Button Text="親から押す" Command="{Binding Command}"/>
        </StackLayout>
    </StackLayout>

</ContentPage>

最後に自作コントロールを貼り付けた親ページの View です。比較用に普通の Label も貼り付けてあります。普通に考えたら子(自作コントロール)の Command は無意味で、親の Command でどちらの BackgroundColor も操作できそうなもんですが…果たして?

予想 現実
f:id:roamschemer:20200121011330g:plain:w300 f:id:roamschemer:20200121011358g:plain:w300

なんと、子の View には BackgroundColor を変化させる記述が一切ないのに Command で BackgroundColor が変化。親の Command では子の BackgroundColor は変化しません。なるほど!これがかむさんの言っていた事!つまりはこういう事ですな!

<!-- MyControlのViewModelと接続されているので親から操作不能 子から操作される-->
<Cont:MyControl BackgroundColor="{Binding BackColor.Value}"/>

<!-- 当然だがこっちは親のViewModelから変更できる -->
<StackLayout BackgroundColor="{Binding BackColor.Value}">
    <Label Text="親見本" />
    <Button Text="親から押す" Command="{Binding Command}"/>
</StackLayout>

不思議な感じだけど納得した!でもじゃあどうすりゃいいのん?

明示的に ViewModel を指定する

これもかむさんが教えてくださいました。教えてもらってばかりだな…

つまりはこういう事です。

[MainPage.xaml]

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:Cont="clr-namespace:ContentViewTest.Views"
             xmlns:ViewModels="clr-namespace:ContentViewTest.ViewModels"
             x:Class="ContentViewTest.Views.MainPage"
             x:Name="self">

    <StackLayout>

        <!--MyControlのViewModelと接続されているので親から操作不能 子から操作される-->
        <Cont:MyControl BackgroundColor="{Binding BackColor.Value}"/>

        <!--明示的にViewを指定するとそれと繋がるViewModelからの操作可能となる-->
        <Cont:MyControl BackgroundColor="{Binding BindingContext.BackColor.Value ,Source={x:Reference self}}"/>

        <!-- こっちは当然親のViewModelから変更できる -->
        <StackLayout BackgroundColor="{Binding BackColor.Value}">
            <Label Text="親見本" />
            <Button Text="親から押す" Command="{Binding Command}"/>
        </StackLayout>
    </StackLayout>

</ContentPage>

親にx:Name="self"という風に名前を付けて、BackgroundColor="{Binding BindingContext.BackColor.Value ,Source={x:Reference self}}"という風にその親の名前を指定すると、それに繋がる ViewModel から操作できる。そう、こんな感じになります。

f:id:roamschemer:20200121012122g:plain:w300

やったぜ!

自作コントロールに配置したプロパティを親から操作する

ここまでわかればあとは簡単!親から子のプロパティを操作しましょう!…と思ったんですが、そうは問屋が卸さないようで…まだできていません。前の記事のように ContentView を継承した MyContentView を作り、その中のプロパティを変化させるところまでは問題ないのですが、じゃあそれをどうやって子の ViewModel と Binding させるんだってところで詰まってしまいました。まぁ次回にご期待ください…(というか、できるんかこれ状態ですがねちくしょう!)

※2020/02/11 続きを書きました www.gunshi.info