- 1. BootstrapperShell
- 2. Region
- 3. Custom Region Adapter
- 4. View Discovey
- 5. View Injection
- 6. ActivationDeactivation
- 参考
WPF + .NET Core (5以降は Core は省略される) で Prism を使ってみよう。
使用している Prism のバージョンは次のとおり:
- 8.1.97 (2021/05/25)
使いはじめるに際して、Prism の開発チームが公開しているサンプルがあるので、それをテストしてみることにしました。
「Prism Full App (.NET Core) テンプレートを体験する」の記事も参考になると思います。
サンプルは DI コンテナに Unity を利用しているみたいです。Unity の名前は ゲームエンジンの Unity と関係がなくて、「IoC の Unity」になります。
ほかにも DryIoc という選択肢があります。機能的な違いは(ほぼ)無いということで、とりあえずさわり始めるときはデフォルトが Unity みたいくらいでよいと思います。
コードに説明がないので、ちょっと困るんだけどなにをしているのか調べていく記事です。MVVM あたりの知識はちょっと必要。
関連記事は以下:
1. BootstrapperShell
まず、公式の解説。
古い Legacy (Prism 6) の説明です。
違いは App が継承していたけど、現在はただのクラスが継承する形式になっています。ざっくりと、これはアプリケーションの起動シーケンスを担うクラスです。
protected override DependencyObject CreateShell() => Container.Resolve<MainWindow>(); protected override void RegisterTypes(IContainerRegistry containerRegistry) { }
CreateShell メソッドで最初のウィンドウを指定しており、これが表示されます。Prism 6 の頃のサンプルコードを読むと、InitializeShell で Window を Show していますが、現在はやらなくてもいいみたいですね。
とりあえず、これが最小の Prism アプリケーション構成になるかと思います。
2. Region
Region のサンプルはとてもシンプルで XAML に以下があるだけ。 RegionManager があることはわかります。しかし、なにをするものなのか、説明が足りません。
<Window x:Class="Regions.Views.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:prism="http://prismlibrary.com/" Title="Shell" Height="350" Width="525"> <Grid> <StackPanel prism:RegionManager.RegionName="ContentRegion" /> </Grid> </Window>
Region がどういう役割をするのかといえば、StackPanel の要素を入れ替える(詰め込む)ための機能になっています。試しに、View を詰め込むサンプルは次のとおり:
まず、Bootstrapper を App で書くとこんな感じ。RegisterTypes に DI コンテナー(表示する画面)を用意します。
public partial class App : PrismApplication { protected override Window CreateShell() { return Container.Resolve<MainWindow>(); } protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterForNavigation<Views.UserControl1>(); containerRegistry.RegisterForNavigation<Views.UserControl2>(); } }
UserControl1.xaml をプロジェクトに追加しています。(どんな画面でもよい)
MainWindow はこんな感じ:
<Window x:Class="Regions.Views.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:prism="http://prismlibrary.com/" Title="Shell" Height="350" Width="525" prism:ViewModelLocator.AutoWireViewModel="True"> <Grid> <ContentControl prism:RegionManager.RegionName="ContentRegion" /> </Grid> </Window>
prism:ViewModelLocator.AutoWireViewModel="True"
の部分が新しく追加されていますが、デフォルトで True みたいです。むしろ、明示的に False にしないと自動で View は ViewModel とバインディングされます。
MainWindow の ViewModel は次のとおり:
using Prism.Mvvm; using Prism.Regions; namespace Regions.ViewModels { public class MainWindowViewModel : BindableBase { private readonly IRegionManager _RegionManager; public MainWindowViewModel(IRegionManager regionManager) { _RegionManager = regionManager; _RegionManager.RegisterViewWithRegion("ContentRegion", typeof(Views.UserControl1)); } } }
これで MainWindow の画面に UserControl1 が表示されます。
注意:このやり方は、サービスロケーターの謗りを免れない恐れがあります。
まとめると、
- DI コンテナーに View を入れた
- ViewModel から View のコンテンツを切り替えた
- View と ViewModel の繋がりは
ContentRegion
というテキストであって、疎結合といえる - Region マネージャーなので Model を管理する DI ではない(はず)
- DI の考え方が必要になるので必要なら Autofac などを確認
3. Custom Region Adapter
2 で利用した Region の機能を拡張するときに使用する機能です。
ConfigureRegionAdapterMappings
がスタート。
protected override void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings) { base.ConfigureRegionAdapterMappings(regionAdapterMappings); regionAdapterMappings.RegisterMapping(typeof(StackPanel), Container.Resolve<StackPanelRegionAdapter>()); }
見ての通り2つのことしかしていないです。
- アプリケーションが利用するデフォルトの RegionAdapterMappings を設定している
- RegionAdapterMappingsに StackPanel 用の Custom Region Adapter を追加している
Custom Region Adapter で作成したクラスは次のとおり:
public class StackPanelRegionAdapter : RegionAdapterBase<StackPanel> { public StackPanelRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory) : base(regionBehaviorFactory) { } protected override void Adapt(IRegion region, StackPanel regionTarget) { region.Views.CollectionChanged += (s, e) => { if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add) { foreach (FrameworkElement element in e.NewItems) { regionTarget.Children.Add(element); } } //handle remove }; } protected override IRegion CreateRegion() { return new AllActiveRegion(); } }
基本的な実装は、サンプルのままでコントロールを表示することができるので、これを拡張する形になります。
個人的には StackPanel より ContentControl で作ったほうがいいんじゃないかと思います。StackPanel は、サイズが自動で Auto (最小のサイズ) になっちゃうので。
DI は 2020 年にも Qiita で話題になり、様々な意見交換がありました。結局、DI を利用している人は多いんで、一家言を持ってる人も多い。でも、DI 自体の定義は抽象的(大きな考えを指してる)なんで、微妙に考え方の違いがあるみたい。
また、サービスロケーターみたいな書き方がアンチパターンに挙がっている(他言語でも言うと思うけど)みたいなこともあるんだけど、情報が散らばっていて「コーディングの基礎を知りたいならリーダブルコードを読んどけ」みたいにいかないのかもしれない。
個人的な考えは以下にまとめた:
4. View Discovey
MainWindow.xaml.cs
で、直接 View をコンテンツに設定している手法のこと。
public MainWindow(IRegionManager regionManager) { InitializeComponent(); //view discovery regionManager.RegisterViewWithRegion("ContentRegion", typeof(ViewA)); }
簡易な手法のひとつになるが Prism Full App Template のように、丁寧にやる方法もあるのでケースバイケースで考えること。
5. View Injection
コンテナとリージョンを直接操作して、追加したり削除したりする。 下手をするとサービスロケータになってしまうため、注意が必要。
public partial class MainWindow : Window { IContainerExtension _container; IRegionManager _regionManager; public MainWindow(IContainerExtension container, IRegionManager regionManager) { InitializeComponent(); _container = container; _regionManager = regionManager; } private void Button_Click(object sender, RoutedEventArgs e) { var view = _container.Resolve<ViewA>(); IRegion region = _regionManager.Regions["ContentRegion"]; region.Add(view); } }
6. ActivationDeactivation
View の表示・非表示を操作することができます。5と比べるとかなり重要。
View 自体も非アクティブ化しても削除したわけではないので、例えば、テキストボックスの入力は前回の状態が残ったままになる。
public partial class MainWindow : Window { IContainerExtension _container; IRegionManager _regionManager; IRegion _region; ViewA _viewA; ViewB _viewB; public MainWindow(IContainerExtension container, IRegionManager regionManager) { InitializeComponent(); _container = container; _regionManager = regionManager; this.Loaded += MainWindow_Loaded; } private void MainWindow_Loaded(object sender, RoutedEventArgs e) { _viewA = _container.Resolve<ViewA>(); _viewB = _container.Resolve<ViewB>(); _region = _regionManager.Regions["ContentRegion"]; _region.Add(_viewA); _region.Add(_viewB); } }
ちなみに、こんな感じで新しい ViewA と差し替えることもできた。
private void Button_Click_4(object sender, RoutedEventArgs e) { var newViewA = _container.Resolve<ViewA>(); if (_viewA != null) { _region.Remove(_viewA); } _region.Add(newViewA); _viewA = newViewA; }
Prism の DI はじめの部分から、VM と V の連携と操作が1~6といった雰囲気だと思います。 次回はモジュール関係。