放浪軍師のアプリ開発局

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

画面遷移時に受け取った Model を ReactiveProperty でバインドしたい

さて始まりました放浪軍師のXamarin.Formsによるアプリ開発Unityやれよ。

画面遷移時に受け取った Model を ReactiveProperty でバインドしたい

簡単にできると思ったのですが中々に苦戦したので備忘録として残しておきます。

環境

Xamarin.Forms 4.4.0.991640
ReactiveProperty 6.2.0
Prism.Unity.Forms 7.2.0.1422

navigationService.NavigateAsync() の基本

Prism には画面遷移する際に便利なnavigationService.NavigateAsync()というメソッドが存在します。ただ単に遷移する際は ViewModel にこう書きます。

await navigationService.NavigateAsync("View"); //Viewは遷移先Viewの名前

そして第二引数に NavigationParameters 型を与える事によって遷移先にパラメータを与える事ができます。

var parameters = new NavigationParameters { { "Model", model } }; //Model をパラメータにして
await navigationService.NavigateAsync("View", parameters); //第二引数にすると遷移先に渡せる

遷移先の ViewModel では OnNavigatedTo() の引数から渡された値を取り出すことができます。

public override void OnNavigatedTo(INavigationParameters parameters) {
    if (parameters == null) return; //nullを確認
    if (parameters.ContainsKey("Model")) Model = (Model)parameters["Model"]; //あれば取り出す
}

ちなみにNavigationParametersDictionaryみたいな感じで複数のパラメータを扱えます。便利だね!

受け取った Model を ReactiveProperty でバインド

さてここからが本題。この引き渡された Model クラスを ReactiveProperty でバインドさせるにはどうしましょうか?率直に書けばこんな感じだと思います。

こんな Model だったとして

public class Model : BindableBase {
    public string Title { get => title; set => SetProperty(ref title, value); }
    private string title;
}

ViewModel でこうやってみる。

public class ViewModel : ViewModelBase {
    private Model Model { get; set; }
    public ReactiveProperty<string> Title { get; }
    public ViewModel(INavigationService navigationService, Model model) : base(navigationService) {
        this.Model = model;
        Title = Model.ObserveProperty(x => x.Title).ToReactiveProperty().AddTo(this.Disposable);
    }
    public override void OnNavigatedTo(INavigationParameters parameters) {
        if (parameters == null) return;
        if (parameters.ContainsKey("Model")) Model = (Model)parameters["Model"];
    }
}

…が、ダメでした。View はうんともスンとも言いません。いけると思ったんですがね。で、この件についていろいろと探していると以下記事にて、その解決策が示されていました。

elf-mission.net

今のコードに適用するとこんな感じです。

public class ViewModel : ViewModelBase {
    private Model Model { get; set; }
    public ReactiveProperty<string> Title { get; set; } //←setter追加
    public ViewModel(INavigationService navigationService, Model model) : base(navigationService) {
        this.Model = model;
        //Title = Model.ObserveProperty(x => x.Title).ToReactiveProperty().AddTo(this.Disposable); //←こっちに書かずに
    }
    public override void OnNavigatedTo(INavigationParameters parameters) {
        if (parameters == null) return;
        if (parameters.ContainsKey("Model")) Model = (Model)parameters["Model"];
        Title = Model.ObserveProperty(x => x.Title).ToReactiveProperty().AddTo(this.Disposable); //←こっちに書いて
        this.RaisePropertyChanged(null); //←Viewにプロパティの変更を通知
    }
}

ただこのやり方がどうにもしっくりこない。折角の ReactiveProperty なのに変更の通知を記述しなければならないし、ReactiveProperty は基本コンストラクタで書きたいよね…。ということで考えてみたのですが、突如閃きました。圧倒的閃き!

解決策

まず Model 内Replacement()というデータを丸ごと差し替える為のメソッドを作成するじゃろ?

public class Model : BindableBase {
    public string Title { get => title; set => SetProperty(ref title, value); }
    private string title;
    //↓丸ごと差し替えるメソッドを作成
    public void Replacement(Model p) {
        Title = p.Title;
    }
}

そしてOnNavigatedTo()にて渡されたパラメータを、今作成したReplacement()にそのまま渡す。

public class ViewModel : ViewModelBase {
    private Model Model { get; } //←setter不要
    public ReactiveProperty<string> Title { get; } //←setter不要
    public ViewModel(INavigationService navigationService, Model model) : base(navigationService) {
        this.Model = model;
        Title = Model.ObserveProperty(x => x.Title).ToReactiveProperty().AddTo(this.Disposable);
    }
    public override void OnNavigatedTo(INavigationParameters parameters) {
        if (parameters == null) return;
        if (parameters.ContainsKey("Model")) Model.Replacement((Model)parameters["Model"]); //パラメータをそのまま渡す
    }
}

これで Replacement()内で値が書き換わるので、ReactiveProperty により Model → ViewModel → View と伝搬するという訳です。なにが圧倒的閃きだ。

いやぁ解ってしまえば何てことありませんが、思いつくまでが中々に骨が折れましたね。解決まで大体3時間ぐらいかかりました。うーん、私もまだまだだなぁ…