もりゃきに後は無い

Prismサンプルを読み解く 第二回(第02章~第06章)

 すみません、WPFでアプリ作ろうとしたりゲームしたりメンタル病んで、記事更新遅れました。いや誰も待ってないだろうけど。

 WPFでMVVMなアプリを作るため、フレームワーク Prism の 公式サンプル を読み解いていきます。 今回は 02-Regions から 06-ViewActivationDeactivation までを読み解いていきます。っていうか読み解けているのか?と疑問符がたくさんつきますが気にしない。

前置き

 誰かの参考になるか?そんなの知らんがな。個人のメモ、広告取ってないんだから好きに書かせて貰います。こういう所で手を抜くと、後々痛い目に遭うからねぇ。

 Visual Studio で フレームワーク Prism が使える環境になってることが前提。前回書き損ねましたが、Prism Version 7.2 です。書いたの大分前ですが 7.2 基準です。

02-Regions

  • 原文「Create a region」
  • 「リージョンの作成」by DeepL…Google翻訳は略

 というか「各サンプルは、前のサンプルのコンセプトをベースにしています(Each sample builds on the previous sample’s concept.)by DeepL」を踏まえると、本当に毎回プロジェクトを作り直す必要があるのか、疑問が湧いてきますね。

 でも大丈夫。第一回が嘘のように「Prism Blank App(.NET Core)」で出てきました、というか等価といって過言ではありません。プロジェクト名は「Resions」です。

プロジェクト再現するための行為

 特にありません。プロジェクト作成で完了です。

 ごく細かいところに違いはあります。例えば MainWindow.xaml の Window タグ属性 prism:ViewModelLocator.AutoWireViewModel="True" がある~とか、この先も細かい Window タグ属性の違いがありますが、Prism サンプルを読み解く上では「問題ありません」。

余談:なんで「細かい違い」が現在問題ないか

 ある程度、完全再現を試みていると複数プロジェクトで同じ違いがあります。具体例を出しますと

  • プロジェクト作成時
<UserControl x:Class="ViewDiscovery.Views.ViewA"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       xmlns:prism="http://prismlibrary.com/"       
       prism:ViewModelLocator.AutoWireViewModel="True">
  <Grid>
      
  </Grid>
</UserControl>
  • Prismサンプル
<UserControl x:Class="ViewDiscovery.Views.ViewA"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
       xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
       xmlns:local="clr-namespace:ViewDiscovery.Views"
       mc:Ignorable="d" 
       d:DesignHeight="300" d:DesignWidth="300">
  <Grid>
    <TextBlock Text="ViewA" FontSize="38"/>
  </Grid>
</UserControl>

 この中で、知識がある人には自明なのかもしれませんが、サンプルを読み解く上で必要な変更は <TextBlock Text="ViewA" FontSize="38"/> この一行だけです。今後進んでいく際の保証はしきれませんけどね。

 具体的な差違については、@ITに岩永信之先生によるXAMLの基礎 に記述がありますが

  • d名前空間 xmlns:d:ビジュアルデザイナ上でだけ使う機能
  • mc名前空間 xmlns:mc :アプリ実行時には不要な機能を無視するための Ignorable属性などを定義

 prism:ViewModelLocator.AutoWireViewModel="True" については ViewModelLocator (英語)にありますが、 ざっくり読んだ限り、標準的な命名規則にするもので Xamarin.Forms 開発の場合必須 という代物です。いや Xamarin 消えるでしょって辺りまでは知らないですけど。

 開発していく上で標準の命名規則が合わない場合もあるとき、ViewModelLocationProvider.Register メソッドを使って、 ViewModemLocator に直接特定のビューへの ViewModel マッピングを登録…などなど書いてありますが、要は Register メソッドがなければ、おそらく読み解く上で支障がないので「細かい違い」と断定して問題なしとします。

xmlns:localについて

 おい、 xmlns:local="clr-namespace:ViewDiscovery.Views" について説明がないぞ、と仰るそこのあなた、そうなんですよ…これに関しては「現状使ってないけど実装次第では使う」という可能性があります。これが具体的に何かというと、 ViewDiscovery.Views へのエイリアス になります。ただし、この章に入っているとおり Prismのサンプルを読み解く上では必要ありません。これは全プロジェクト開いて確認しました。

 ごく一部の例外、具体的には 28-CustomRequest を除き、App.xaml では xmlns:local があるけど使ってない、他の .xaml ファイルでは xmlns:local が無いという状況です。28-CustomRequest が例外というのはは、カスタムコントロールにも xmlns:local が含まれているけど使っていない、というだけです。ご安心ください。

Region 例の問題

 ここで問題なのは「Regionの作成」と言いながら、 Regionとは何なのか について一切見えないことですね。え? 公式ドキュメント 読めですか?…あーすみません、 そういう方と自分は、そもそものレベルが違うんですよレベルが。 英語をモノともしなくて、新しい技術を学ぶことにも困ってない、そういう高みにいる方とは違うんですよ、自分は。あなたとはレベルが違うんですよレベルが!

 取り乱しました…というわけで、他力本願で ::halation ghost:: さんを頼りましょう。 Prism の基本的な用語 によると

  • Resion 「Viewを読み込む先のコンテナコントロール」「RegionNameを指定するとPrism内部で管理・識別される」
  • Bootstrapper:「様々な Prism コンポーネントとサービスの初期化」
  • Shell 「Moduleを読み込むWindow」「メインウィンドウやスタートアッププロジェクトを指して Shell と呼ぶ事が多い」
  • Module 「独立して開発、テスト、配布できる機能単位のパッケージ」「多くの例として Shell 内に読み込む View を含んだ DLL プロジェクト」

 覚えなあかん四天王最後の一人 Moduleについては、正直、書いてる時点では自分でもわかってません。ただし、全プロジェクト構成を見たところ、Module は頻出しているので、Prism開発の肝となることでしょう。

 Bttostrapper が初期化し ShellRegionModule を制御する、という関係でしょうか?ここは、Prismサンプルを読み解くことで、何か姿が見えることを期待したいですね。

03-CustomRegions

  • 原文「Create a custom region adapter for the StackPanel」
  • 「StackPanel 用のカスタム リージョン アダプタを作成します」by DeepL
  • 「StackPanelのカスタム領域アダプターを作成する」by Google翻訳

 hu普通に 原文 > DeepL > Google翻訳 ですね。ぶっちゃけ「custom region adapter」がどういうことか全くわからないですけど、Region は「Viewを読み込む先のコンテナコントロール」なので、読み解くことでわかることを期待しつつ。

Regionsプロジェクトを再現するために必要な変更ファイルと内容

 冒頭でUsing System.Windows.Controls;宣言をします。

 App クラスに以下を追加します

protected override void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings)
{
  base.ConfigureRegionAdapterMappings(regionAdapterMappings);
  regionAdapterMappings.RegisterMapping(typeof(StackPanel), Container.Resolve<StackPanelRegionAdapter>());
}

 ただし当然ながら、Prism/StackPanelResionAdapter.cs を書かないと、諸々の補完はほとんど効きません。そもそもの StackPanel が XAML に記述されてないのですから。

 文章にしたらグダグダ長くなるので、大切な所(というかほぼ全部)をコピペします。

namespace Regions
{
  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
      };
    }

    // RegionAdapterBase で必要(コメントもりゃき追加)
    protected override IRegion CreateRegion()
    {
      return new AllActiveRegion();
    }
  }
}

さて、これは何をやっているのやら?

 困ったことに、ビルドして実行しても何も変化はありません。見た目で何か判断するのは厳しそうです。ただし、動かした手は嘘をつかない。調べた内容も信用できれば問題無い。説明文を振り返ると「Create a custom region adapter for the StackPanel」です、ということは StackPanelをリージョンに適合させて使えるようにする というようなことをやっているのでしょう。

 Adapt の処理は、StackPanelに追加されたコントロールを StackPanel に登録する、という処理に見えます。

 StackPanelRegionAdapter は RegionAdapterBase&ltT>.Adapt を継承しています。ここでの T は StackPanel ですね。

 RegionAdapterBase<T> は IRegion に適応させる abstract method です。IResionインターフェースは、INavigateAsync と INotifyPropertyChanged インターフェースを持っています。

 INotifyPropertyChanged と言えば、プロパティの変更通知をするものでした。おそらくですが、Prismの設計で StackPanel を使う時には、このようにカスタムリージョンを実装するのが定石となるのではないでしょうか?

 :: halation ghost:: さんも、ここは「最初から理解する必要なし。必要に応じて参照すれば良い」と仰ってます。こんな所で十分でしょう。

04-ViewDiscovery

  • 原文「Automatically inject views with View Discovery」
  • 「View Discoveryで自動的にビューを注入します」by DeepL
  • 「ビューディスカバリを使用してビューを自動的に挿入します」by Google翻訳

 この一文では、精々「何か自動的にやるんだな」程度で何が何だかわかりません。とっととコードを再構築してみましょう。

 新規プロジェクト Prism Blank App(.NET Core) を「ViewDiscovery」の名前で作成します。

ViewDiscoveryプロジェクトを再現するために必要な追加ファイルと内容

 先に Views/ViewA コントロールを作る必要があります!

Views/ViewA.xaml

 先に書いたとおり細かい違いはありますが

  <Grid>
      
  </Grid>

これを

  <Grid>
    <TextBlock Text="ViewA" FontSize="38"/>
  </Grid>

 と TextBlock を追加するだけでOKです。Views/ViewA.xaml.cs も追加されますが、特にやることはありません。

ViewDiscoveryプロジェクトを再現するために必要な変更ファイルと内容

 ViewA UserControl を作ったら自動で追加されますが、コンストラクタに引数が必要になるので

  public MainWindow()

 を

  public MainWindow(IRegionManager regionManager)

 に変更しましょう。参照(Using) は自動で追加されるようです。

実行してみる

 これで左上に「ViewA」と書かれたウィンドウが開かれたと思います。

実行画面

 さて、今まで自分が手を動かしたことを、意味としてまとめましょう。

  • ViewA コントロールの作成
  • 自動で MainWinodw に ViewA が登録される(登録させるために、ViewAを先に作る必要があった)
  • ViewA コントロールのXAML編集、ここにTextBlockで「ViewA」とテキスト設定をする

 そう、 俺たちはいつの間にかView(ViewA)をRegionに登録していたんだよ!

 まあ、こうして動かしてみると、説明文の意味も見えてくるというものです。コントロールを作ったら、自動的に View を注入しますよ、それが View Discovery ってもんですよ、というお話だったのでしょう。

05-ViewInjection

  • 原文「Manually add and remove views using View Injection」
  • 「ビューインジェクションを使用してビューを手動で追加・削除」by DeepL
  • 「ビューインジェクションを使用して手動でビューを追加および削除する」by Google翻訳

 まあ、前章では自動でView注入してたから、今度はView Injectionを使ってViewを手動で追加・削除しましょう、という内容っぽいですね。

 新規プロジェクト Prism Blank App(.NET Core) を「ViewInjection」の名前で作成します。

ViewInjectionプロジェクトを再現するために必要な追加ファイルと内容

 先にこちらの操作で Views/ViewA コントロールを作る必要があります!

 Viewsディレクトリから「新しい項目」で「Prism UserControl (WPF)」を選びます。は「ViewA(.xaml)」

Views/ViewA.xaml

<UserControl ...>
  <Grid>

  </Grid>
</UserControl>

 Grid内にTextBlockを配置します。

<UserControl ...>
  <Grid>
  <TextBlock Text="View A" FontSize="38" />
  </Grid>
</UserControl>

 同時に Views/ViewA.xaml.cs が追加されますが、特にやることはありません。

ViewInjectionプロジェクトを再現するために必要な変更ファイルと内容

  /// <summary>
  /// Interaction logic for App.xaml
  /// </summary>
  public partial class App
  {

 冒頭に Using Prism.Unity; を追加し、App クラス を PrismApplication からの継承に変更します。

  /// <summary>
  /// Interaction logic for App.xaml
  /// </summary>
  public partial class App : PrismApplication
<Window ...
    Title="{Binding Title}" Height="350" Width="525" >
  <Grid>
    <ContentControl prism:RegionManager.RegionName="ContentRegion" />
  </Grid>
</Window>

 以下のように書き換えます、実際はGridタグとその中の書き換えだけでいいです。

<Window ...
    Title="Shell" Height="350" Width="525">
  <DockPanel LastChildFill="True">
    <Button DockPanel.Dock="Top" Click="Button_Click">Add View</Button>
    <ContentControl prism:RegionManager.RegionName="ContentRegion" />
  </DockPanel>
</Window>

余談:Titleについて

 ここではXAMLに Title="Shell" と直接書き換えていますが、Views/MainWindowViewModel.cs の

  public class MainWindowViewModel : BindableBase
  {
    private string _title = "Prism Application";
    public string Title
    {
      get { return _title; }
      set { SetProperty(ref _title, value); }
    }
    
  }

 この _title"Prism Application""Shell" に書き換えても動きは同じです、というか元コードはここからデータバインディングで設定しているわけですから。これはどちらが正解かというと、状況次第で答えが変わると思います。

 今回のように1ウィンドウのタイトル変更無し、という状況なら、直接値を書き換えた方がデザインの把握のしやすさが優れているでしょう。しかし、画面遷移が起こる規模(そしてPrismを使う時点でその規模になりうる)では、プロパティにして、最初のタイトルをここで設定した方が「タイトルを動的に変更できる」メリットを享受できるでしょう。

 まあ、Prism を触ろうとしている時点で WPF のデータバインディングなんて知ってるよと言われたらそれまでですが、場所はここですよ、ということでご容赦を。

using Prism.Ioc;
using Prism.Regions;

 これを追加して、以下のようにMainWindowクラスを書き換えます。

  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);
  }
  }

 MainWindowクラスのコンストラクタ引数が増えていることに注意してください。

このプロジェクトを動かして

 見た目の動きは「ボタンが上部にあり、クリックしたら View A と表示される」ですね。

 手を動かした内容としては

  • ViewAというviewを作成
  • ViewA に TextBlock を配置
  • Views/MainWindow.xaml で ButtonContentContron を配置
  • Views/MainWIndow.xaml.cs で、ボタンが押されたとき ViewA のインジェクション(注入)

 ここで、処理がViewの追加だけなのは、サンプルとして単純化するためでしょう。

 MainWindow.xaml にあった

    <ContentControl prism:RegionManager.RegionName="ContentRegion" />

 これが Region のようですね。

 このサンプルを弄れば「ViewAの表示と非表示の切り替え」といった処理もできそうです。

06-ViewActivationDeactivation

  • 原文「Manually activate and deactivate views」
  • 「ビューの有効化と無効化を手動で行う」by DeepL
  • 「ビューを手動でアクティブ化および非アクティブ化する」by Google翻訳

 プログラマ視点での訳としては、Google翻訳の方がしっくり来ますかね?日本語としては DeepL の方が流暢ですけど。

 新規プロジェクト Prism Blank App(.NET Core) を「ActivationDeactivation」の名前で作成します。

ViewActivationDeactivationプロジェクトを再現するために必要な追加ファイルと内容

 それぞれ、Viewsディレクトリから「新しい項目」で「Prism UserControl (WPF)」を選び、今回は2つのコントロールを作ります。名前は「ViewA.xaml」と「ViewB.xaml」

View/ViewA.xaml

  <Grid>
      
  </Grid>

 TextBlockタグを Text="View A" で追加します。

  <Grid>
    <TextBlock Text="View A" FontSize="38" />
  </Grid>

View/ViewB.xaml

  <Grid>
      
  </Grid>

 今度は、TextBlockタグを Text="View B" で追加します。

  <Grid>
    <TextBlock Text="View B" FontSize="38" />
  </Grid>

 View/ViewA.xaml.cs と View/ViewB.xaml.cs に変更点はありません。

ViewActivationDeactivationプロジェクトを再現するために必要な変更ファイルと内容

 前回と同様に、冒頭にUsing Prism.Unity; を追加し、public partial class Apppublic partial class App : PrismApplication と継承させます。

 <Grid> 内を以下のように変更します。。

<DockPanel LastChildFill="True">
  <StackPanel>
  <Button Content="Activate ViewA" Click="Button_Click"/>
  <Button Content="Deactivate ViewA" Click="Button_Click_1"/>
  <Button Content="Activate ViewB" Click="Button_Click_2"/>
  <Button Content="Deactivate ViewB" Click="Button_Click_3"/>
  </StackPanel>
  <ContentControl prism:RegionManager.RegionName="ContentRegion" HorizontalAlignment="Center" VerticalAlignment="Center" />
</DockPanel>

 どうでもいいですが、Button_Click の名前が…実際のコーディング時には、もうちょっとどうにかしましょうね。

 宣言を追加します。

using Unity;
using Prism.Regions;
using Prism.Ioc;

 メンバ変数を追加します。

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);
  }

 Activate/Deactivate View A/B の処理を書きます

  private void Button_Click(object sender, RoutedEventArgs e)
  {
    //activate view a
    _region.Activate(_viewA);
  }

  private void Button_Click_1(object sender, RoutedEventArgs e)
  {
    //deactivate view a
    _region.Deactivate(_viewA);
  }

  private void Button_Click_2(object sender, RoutedEventArgs e)
  {
    //activate view b
    _region.Activate(_viewB);
  }

  private void Button_Click_3(object sender, RoutedEventArgs e)
  {
    //deactivate view b
    _region.Deactivate(_viewB);
  }

 以上です、お疲れ様でした。

この動きからわかること

 動きを見るまでもなく、 Views/MainWindow.xaml.cs が色々やってるとわかります。Activate ViewA(Button_Click) で ViewA を有効にし、 Deactivate ViewA(Button_Click_1) で ViewA を無効にしています。ViewB も同様の動きですね。

 そして「Activate ViewA」→「Activate ViewB」→「Deactivate ViewB」としても ViewA とは表示されません。それはまあ、もしかしたら当然の話かもしれません。

 前節で見た通り Regionは1つしかない のだから、他のビューをアクティブにすれば他のビューがアクティブでなくなる、というだけの話かもしれません。ごめんなさい、これ以上はわかりません。

まとめ

 02-Regions, 03-CustomRegions については、StackPanel を Region に適合させるのかな?程度しかわからず、実質読み解けませんでした。ごめんなさい。

 04-ViewDiscovery では、ユーザーコントロールを作り Region に登録することで、自動的にユーザーコントロールを表示させました。

 05-ViewInjection では、ユーザーコントロールを作り、手動でコントロール表示を切り替えました。

 06-ViewActivationDeactivation では、2つのビューを作り、有効化と無効化の実装、そして動きを観察しました。

 俯瞰して見ると、MainWindows.xaml にある ContentControl に対し、コントロールを新たに作って登録する、という流れが見えてきます。主にコントロールの自動登録、手動登録、有効化と無効化、というのが今回記事の範囲でした。

 次回はサンプルでも複数のプロジェクトに分かれている 07-Modules を読み解こうとする予定です。

 最後に:長文、大変失礼しました…