さて、始まりました放浪軍師のアプリ開発。Unity 始めるよと言った矢先に Xamarin.Forms です!まだ解決してなかったからね!過去の記事はこちら。
www.gunshi.info www.gunshi.info
自作コントロールに配置したプロパティを親から操作したいが…どうすんのさ
前回、前々回と ContentView を用いた MVVM による自作コントロールについて色々と頑張ってみましたがどうにも上手くいかない。こりゃあ完全に手詰まりだ!な状態だったのですが、 spacekey (@dlspacekey) | Twitter さんが助け舟を出してくれました。しかも非常に丁寧な記事で書いてくださいました。ほんともう感謝しかない!
お陰で一気に解決に向かいましたのでこれを踏まえてサンプルコードを作成してみようと思います。…とはいえ、ごらんのとおり全部記事にて書いてくれていますので、さっくり作成してしまいます。
環境
Xamarin.Forms 4.4.0.991640
ReactiveProperty 6.2.0
Prism.Unity.Forms 7.2.0.1422
GitHub
こちらがサンプルを見ながら作り直した物になりますのであわせてご確認ください。 github.com
デモ
今回公開しているサンプルはこんな感じで動きます。
Android | UWP |
---|---|
子から個別抽選。親から全体抽選を行っています。ちゃんと動いていますよやったぜ!おしまい!!!って訳にはいきませんよね。ここからは気になったところを調べていきます。
自作コントロール(ContentView)は Prism を使わず作成
思わず唸りました。なるほど、これは前回紹介したように混乱のもとになるからですね~。Prism を使わない場合は当然ながら自力で View と ViewModel を BindingContext で結び付ける必要がありますが、自作コントロールの場合はむしろ好都合となりますね。…ただ、それだけじゃない事情ものちに発覚します。
自作コントロールの ViewModel を BindingContext に Binding させる
親のView
<StackLayout> <CollectionView ItemsSource="{Binding RandomVTuberViewModels}"> <CollectionView.ItemsLayout> <GridItemsLayout Orientation="Vertical" Span="3" /> </CollectionView.ItemsLayout> <CollectionView.ItemTemplate> <DataTemplate> <views:RandomVTuberView BindingContext="{Binding}"/> <!-- ここでViewModelをBinding --> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView> <Button Text="全部推す!" Command="{Binding AllRundomCommand}"/> </StackLayout>
親のViewModel
public ReadOnlyReactiveCollection<RandomVTuberViewModel> RandomVTuberViewModels { get; } (略) RandomVTuberViewModels = coreModel.VTuberRandoms.ToReadOnlyReactiveCollection(x => new RandomVTuberViewModel(x));
おおおお!こんな方法があったのか!という感じです。目から鱗。自作コントロールは当然ながら親の配下にあるためこのような事を行っても不自然じゃないんですね。MVVM = ViewModel 間の操作はあり得ない!という思想に凝り固まっていたのかもしれません。
ViewModel のコンストラクタにある引数の謎
ところで、Prismを用いて作成した ViewModel のコンストラクタではこのような記述をしています。
public FastPageViewModel(INavigationService navigationService,CoreModel coreModel) : base(navigationService) {(略)}
実はこれいろんな記事でもこういう書き方をよく見かけていてずっと気になっていたのですが、この引数の CoreModel coreModel
の部分。いや、そもそもINavigationService navigationService
の部分もですが。これ、一体どこから渡しているんでしょうか?引数にあるという事は当然どこかでインスタンスを生成して引き渡しているはずなのですが、それが何処にも見つかりません。自分はそれが気持ちが悪いのでいつもは以下のように書いていました。これでも問題なく動作します。
private CoreModel coreModel = new CoreModel(); //←ローカルなインスタンスを生成して使う public FastPageViewModel(INavigationService navigationService) : base(navigationService) {(略)}
流石に気になって調べてみたのですが、これ非常に重要でした。 MVVM の説明でよく見かける DI (Dependency Injection = 依存性の注入) とはこれの事だったんですね!
DI と DIコンテナ
DI が何故重要なのか?何故 MVVM で DI の話が出てくるのか?これをわかりやすく説明してくれているのは、かずき(Kazuki Ota) (@okazuki) | Twitterさんが書かれたこの登壇資料じゃないかと思います。ほんといつもお世話になってます!
techcommunity.microsoft.com ※2020/02/12 かずきさんからの報告により正規のページに差し替えました。
上記記事で充分わかると思うので詳細は説明しませんが、超ザックリいうと
- MVVM で疎結合を保つとクラス単位でのテストがしやすい
- でもクラス内で他のクラスを new すると疎結合にならない
- クラス内で new しなくても使えるようにする仕組みが DI(Dependency Injection = 依存性の注入)
- DI をそのまま実装するのは非常に難しいのでライブラリに頼る
- その仕組みを DI コンテナと呼ぶ
という事のようです。ちなみにこのDIコンテナは Prism に内臓されており、テンプレートから作成する際にはコンテナの選択欄が表示されます(今まで適当に選んでいた)。
一般的には Unity というライブラリを使用するようですね。次回から Unity やると言っていたのは本当だったんですね?
DIの使い方
Prism を使っている場合、App.xaml.cs にある RegisterTypes メソッド内にて管理します。ただ、Page 追加時に Prism を使っていれば、View や ViewModel に関しては自動で登録されるので気にすることなくご使用いただける様子。Model に関しては勝手にやってくれているようで、今のところデータクラスをシングルトンにする場合ぐらいにしか気にする必要は無さそうです。ちなみにインスタンス生成時に引数を渡す方法は存在するのですが、その場合は使い勝手が著しく低下してしまうので Model クラスのコンストラクターには引数を使わないようにした方が無難。やり方は以下のような感じでした。
protected override void RegisterTypes(IContainerRegistry containerRegistry) { //Pageも実はDIコンテナに登録されていて、そちらから呼び出されていたらしい。PrismからPageを作ると自動でここに記述が追加される。 containerRegistry.RegisterForNavigation<NavigationPage>(); containerRegistry.RegisterForNavigation<MainPage, MainPageViewModel>(); containerRegistry.RegisterForNavigation<FastPage, FastPageViewModel>(); // 引数無しでシングルトンに指定する //containerRegistry.RegisterSingleton<CoreModel>(); // 引数渡すならこっち(但し基本的には使わないようにした方が良い) //var container = containerRegistry.GetContainer(); //container.RegisterType<CoreModel>(new InjectionConstructor(20)); // DIに引数付ける //container.RegisterSingleton<CoreModel>(new InjectionConstructor(20)); // DIに引数付けてシングルトン }
ちなみに、今回作成した自作クラスは、自力で依存性の注入を行う必要があります。
public FastPageViewModel(INavigationService navigationService,CoreModel coreModel) : base(navigationService) { coreModel.Set(9); RandomVTuberViewModels = coreModel.VTuberRandoms.ToReadOnlyReactiveCollection(x => new RandomVTuberViewModel(x));
うん、なるほど。この場合は親のViewModel から x を渡している。つまり依存性を注入していると。よくできているな…。
シングルトン
さて上に出てきたシングルトンですが、こちらはDIコンテナから呼び出す際に1度だけインスタンスを生成し、以降は使いまわす仕組みです。全体設定などで使用すると良さそうですね。小規模なら永続化にも使えそうですが、なんせグローバル変数に近いですからね。乱用は厳禁でしょう。
↑:こんな感じで簡単に値を保持できるが…乱用は危険
まとめ
という事で、ちょっと横道にも逸れましたが無事自作コントロールを親から操作する事が可能になりました!これで開発も大いに捗りますね!今回わざわざ記事まで書いて教えてくださった spacekey (@dlspacekey) | Twitter さんに再度感謝致します。ありがとー!!!