sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

WPF Prism サンプルコードの学習1 (Bootstrapper, Region)

f:id:shikaku_sh:20211013180838p:plain:w400

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といった雰囲気だと思います。 次回はモジュール関係。

参考