本文は転載記事です
原文著者: RyzenAdorer
原文タイトル: .NET Core 3 WPF MVVM フレームワーク Prism シリーズのリージョンマネージャー
本記事では、.NET Core3 環境で MVVM フレームワーク Prism のリージョンマネージャーを使用した View の管理方法について説明します。
1. リージョンマネージャー
これまでの Prism シリーズでは、標準的な Prism プロジェクトを構築しました。この記事では、以前のプロジェクトで使用した、リージョンマネージャーを利用して View をより適切に管理する方法について解説します。同様に、公式が提示するモデル図を見てみましょう。

ここで分かることは、リージョンマネージャー RegionManager がコントロールに対してリージョンを作成する際の要点は、おおよそ以下の通りです。
- リージョンを作成するコントロールは、RegionAdapter アダプターを含んでいる必要があります
- リージョンは、RegionAdapter を持つコントロールに依存しています
実際に公式の紹介とソースコードを確認したところ、デフォルトの RegionAdapter は 3 つあり、カスタムの RegionAdapter もサポートされています。そのため、公式のモデル図にいくつか補足を加えました。

2. リージョンの作成と View の注入
まず、以前のプロジェクトでのリージョンの分割、およびリージョンの作成と View の注入方法を見てみましょう。

メインウィンドウ全体を 4 つのリージョンに分割しました。
- ShowSearchPatientRegion:ShowSearchPatient ビューを注入
- PatientListRegion:PatientList ビューを注入
- FlyoutRegion:PatientDetail と SearchMedicine ビューを注入
- ShowSearchPatientRegion:ShowSearchPatient ビューを注入
Prism では、リージョンの作成と View の注入を実現する方法が 2 つあります。
- ViewDiscovery
- ViewInjection
2.1. ViewDiscovery
PatientListRegion の作成と View 注入のコードを抜粋します(詳細はデモのソースコードを参照してください)。
MainWindow.xaml:
<ContentControl
Grid.Row="2"
prism:RegionManager.RegionName="PatientListRegion"
Margin="10"
/>
これは、コードビハインド MainWindow.cs で次のように記述するのと同等です。
RegionManager.SetRegionName(ContentControl, "PatientListRegion");
PatientModule.cs:
public class PatientModule : IModule
{
public void OnInitialized(IContainerProvider containerProvider)
{
var regionManager = containerProvider.Resolve<IRegionManager>();
//PatientList
regionManager.RegisterViewWithRegion(RegionNames.PatientListRegion, typeof(PatientList));
//PatientDetail-Flyout
regionManager.RegisterViewWithRegion(RegionNames.FlyoutRegion, typeof(PatientDetail));
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
}
}
2.2. ViewInjection
MainWindow フォームの Loaded イベントで ViewInjection 方式を使用して、View PatientList を注入します。
MainWindow.xaml:
<i:Interaction.Triggers> <i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding LoadingCommand}"/>
/i:EventTrigger>
</i:Interaction.Triggers>
MainWindowViewModel.cs:
private IRegionManager _regionManager;
private IRegion _paientListRegion;
private PatientList _patientListView;
private DelegateCommand _loadingCommand;
public DelegateCommand LoadingCommand =>
_loadingCommand ?? (_loadingCommand = new DelegateCommand(ExecuteLoadingCommand));
void ExecuteLoadingCommand()
{
_regionManager = CommonServiceLocator.ServiceLocator.Current.GetInstance<IRegionManager>();
_paientListRegion = _regionManager.Regions[RegionNames.PatientListRegion];
_patientListView = CommonServiceLocator.ServiceLocator.Current.GetInstance<PatientList>();
_paientListRegion.Add(_patientListView);
}
両者の違いは明らかです。ViewDiscovery 方式は自動的に View をインスタンス化して読み込むのに対し、ViewInjection 方式は手動で View の注入と読み込みのタイミングを制御できます(上記の例では Loaded イベントを使用しています)。公式が推奨するそれぞれの使用シーンは以下の通りです。
ViewDiscovery:
- View の自動読み込みが必要または要求される場合
- View の単一インスタンスがリージョンに読み込まれる場合
ViewInjection:
- View を作成および表示するタイミングを明示的またはプログラムで制御する必要がある場合、またはリージョンから View を削除する必要がある場合
- 同じ View の複数のインスタンスをリージョンに表示する必要があり、各 View インスタンスが異なるデータにバインドされる場合
- View を追加するリージョンのどのインスタンスを制御する必要がある場合
- アプリケーションがナビゲーション API を使用する場合(後述)
3. ビューのアクティブ化と非アクティブ化
3.1. Activate と Deactivate
まず、PatientList と MedicineMainContent の 2 つの View のアクティブ状態を制御する必要があります。コードを示します。
MainWindow.xaml:
<StackPanel Grid.Row="1">
<button
Content="Load MedicineModule"
FontSize="25"
Margin="5"
Command="{Binding LoadMedicineModuleCommand}"
/>
<UniformGrid Margin="5">
<button
Content="ActivePaientList"
Margin="5"
Command="{Binding ActivePaientListCommand}"
/>
<button
Content="DeactivePaientList"
Margin="5"
Command="{Binding DeactivePaientListCommand}"
/>
<button
Content="ActiveMedicineList"
Margin="5"
Command="{Binding ActiveMedicineListCommand}"
/>
<button
Content="DeactiveMedicineList"
Margin="5"
Command="{Binding DeactiveMedicineListCommand}"
/>
</UniformGrid>
</StackPanel>
<ContentControl
Grid.Row="2"
prism:RegionManager.RegionName="PatientListRegion"
Margin="10"
/>
<ContentControl
Grid.Row="3"
prism:RegionManager.RegionName="MedicineMainContentRegion"
/>
MainWindowViewModel.cs:
private IRegionManager _regionManager;
private IRegion _paientListRegion;
private IRegion _medicineListRegion;
private PatientList _patientListView;
private MedicineMainContent _medicineMainContentView;
private bool _isCanExcute = false;
public bool IsCanExcute
{
get { return _isCanExcute; }
set { SetProperty(ref _isCanExcute, value); }
}
private DelegateCommand _loadingCommand;
public DelegateCommand LoadingCommand =>
_loadingCommand ?? (_loadingCommand = new DelegateCommand(ExecuteLoadingCommand));
private DelegateCommand _activePaientListCommand;
public DelegateCommand ActivePaientListCommand =>
_activePaientListCommand ?? (_activePaientListCommand = new DelegateCommand(ExecuteActivePaientListCommand));
private DelegateCommand _deactivePaientListCommand;
public DelegateCommand DeactivePaientListCommand =>
_deactivePaientListCommand ?? (_deactivePaientListCommand = new DelegateCommand(ExecuteDeactivePaientListCommand));
private DelegateCommand _activeMedicineListCommand;
public DelegateCommand ActiveMedicineListCommand =>
_activeMedicineListCommand ?? (_activeMedicineListCommand = new DelegateCommand(ExecuteActiveMedicineListCommand)
.ObservesCanExecute(() => IsCanExcute));
private DelegateCommand _deactiveMedicineListCommand;
public DelegateCommand DeactiveMedicineListCommand =>
_deactiveMedicineListCommand ?? (_deactiveMedicineListCommand = new DelegateCommand(ExecuteDeactiveMedicineListCommand)
.ObservesCanExecute(() => IsCanExcute));
private DelegateCommand _loadMedicineModuleCommand;
public DelegateCommand LoadMedicineModuleCommand =>
_loadMedicineModuleCommand ?? (_loadMedicineModuleCommand = new DelegateCommand(ExecuteLoadMedicineModuleCommand));
/// <summary>
/// フォーム読み込みイベント
/// </summary>
void ExecuteLoadingCommand()
{
_regionManager = CommonServiceLocator.ServiceLocator.Current.GetInstance<IRegionManager>();
_paientListRegion = _regionManager.Regions[RegionNames.PatientListRegion];
_patientListView = CommonServiceLocator.ServiceLocator.Current.GetInstance<PatientList>();
_paientListRegion.Add(_patientListView);
_medicineListRegion = _regionManager.Regions[RegionNames.MedicineMainContentRegion];
}
/// <summary>
/// medicineMainContent ビューを非アクティブ化
/// </summary>
void ExecuteDeactiveMedicineListCommand()
{
_medicineListRegion.Deactivate(_medicineMainContentView);
}
/// <summary>
/// medicineMainContent ビューをアクティブ化
/// </summary>
void ExecuteActiveMedicineListCommand()
{
_medicineListRegion.Activate(_medicineMainContentView);
}
/// <summary>
/// patientList ビューを非アクティブ化
/// </summary>
void ExecuteDeactivePaientListCommand()
{
_paientListRegion.Deactivate(_patientListView);
}
/// <summary>
/// patientList ビューをアクティブ化
/// </summary>
void ExecuteActivePaientListCommand()
{
_paientListRegion.Activate(_patientListView);
}
/// <summary>
/// MedicineModule を読み込み
/// </summary>
void ExecuteLoadMedicineModuleCommand()
{
_moduleManager.LoadModule("MedicineModule");
_medicineMainContentView = (MedicineMainContent)_medicineListRegion.Views
.Where(t => t.GetType() == typeof(MedicineMainContent)).FirstOrDefault();
this.IsCanExcute = true;
}
実行結果は次の通りです。

3.2. ビューのアクティブ状態の監視
Prism では、View に IActiveAware インターフェースを実装することで、ビューのアクティブ状態を監視することもできます。例として、MedicineMainContent ビューのアクティブ状態を監視します。
MedicineMainContentViewModel.cs:
public class MedicineMainContentViewModel : BindableBase,IActiveAware
{
public event EventHandler IsActiveChanged;
bool _isActive;
public bool IsActive
{
get { return _isActive; }
set
{
_isActive = value;
if (_isActive)
{
MessageBox.Show("ビューがアクティブになりました");
}
else
{
MessageBox.Show("ビューが非アクティブになりました");
}
IsActiveChanged?.Invoke(this, new EventArgs());
}
}
}

3.3. Add と Remove
上記の例では ContentControl を使用しましたが、次は ItemsControl を使用した例を示します。コードは以下の通りです。
MainWindow.xaml:
<metro:MetroWindow.RightWindowCommands>
<metro:WindowCommands x:Name="rightWindowCommandsRegion" />
</metro:MetroWindow.RightWindowCommands>
MainWindow.cs:
public MainWindow()
{
InitializeComponent();
var regionManager= ServiceLocator.Current.GetInstance<IRegionManager>();
if (regionManager != null)
{
SetRegionManager(regionManager, this.flyoutsControlRegion, RegionNames.FlyoutRegion);
//WindowCommands コントロールリージョンを作成
SetRegionManager(regionManager, this.rightWindowCommandsRegion, RegionNames.ShowSearchPatientRegion);
}
}
void SetRegionManager(IRegionManager regionManager, DependencyObject regionTarget, string regionName)
{
RegionManager.SetRegionName(regionTarget, regionName);
RegionManager.SetRegionManager(regionTarget, regionManager);
}
ShowSearchPatient.xaml:
<StackPanel
x:Class="PrismMetroSample.MedicineModule.Views.ShowSearchPatient"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
xmlns:const="clr-namespace:PrismMetroSample.Infrastructure.Constants;assembly=PrismMetroSample.Infrastructure"
Orientation="Horizontal"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
prism:ViewModelLocator.AutoWireViewModel="True"
>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding ShowSearchLoadingCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<CheckBox IsChecked="{Binding IsShow}" />
<button
Command="{Binding ApplicationCommands.ShowCommand}"
CommandParameter="{x:Static const:FlyoutNames.SearchMedicineFlyout}"
>
<StackPanel Orientation="Horizontal">
<image
Height="20"
Source="pack://application:,,,/PrismMetroSample.Infrastructure;Component/Assets/Photos/按钮.png"
/>
<TextBlock
Text="Show"
FontWeight="Bold"
FontSize="15"
VerticalAlignment="Center"
/>
</StackPanel>
</button>
</StackPanel>
ShowSearchPatientViewModel.cs:
private IApplicationCommands _applicationCommands;
private readonly IRegionManager _regionManager;
private ShowSearchPatient _showSearchPatientView;
private IRegion _region;
public IApplicationCommands ApplicationCommands
{
get { return _applicationCommands; }
set { SetProperty(ref _applicationCommands, value); }
}
private bool _isShow=true;
public bool IsShow
{
get { return _isShow=true; }
set
{
SetProperty(ref _isShow, value);
if (_isShow)
{
ActiveShowSearchPatient();
}
else
{
DeactiveShowSearchPaitent();
}
}
}
private DelegateCommand _showSearchLoadingCommand;
public DelegateCommand ShowSearchLoadingCommand =>
_showSearchLoadingCommand ?? (_showSearchLoadingCommand = new DelegateCommand(ExecuteShowSearchLoadingCommand));
void ExecuteShowSearchLoadingCommand()
{
_region = _regionManager.Regions[RegionNames.ShowSearchPatientRegion];
_showSearchPatientView = (ShowSearchPatient)_region.Views
.Where(t => t.GetType() == typeof(ShowSearchPatient)).FirstOrDefault();
}
public ShowSearchPatientViewModel(IApplicationCommands applicationCommands,IRegionManager regionManager)
{
this.ApplicationCommands = applicationCommands;
_regionManager = regionManager;
}
/// <summary>
/// ビューをアクティブ化
/// </summary>
private void ActiveShowSearchPatient()
{
if (!_region.ActiveViews.Contains(_showSearchPatientView))
{
_region.Add(_showSearchPatientView);
}
}
/// <summary>
/// ビューを非アクティブ化
/// </summary>
private async void DeactiveShowSearchPaitent()
{
_region.Remove(_showSearchPatientView);
await Task.Delay(2000);
IsShow = true;
}
実行結果は次の通りです。

ここでの WindowCommands の継承チェーンは次の通りです。WindowCommands <-- ToolBar <-- HeaderedItemsControl <-- ItemsControl。Prism のデフォルトアダプターには ItemsControlRegionAdapter が含まれているため、そのサブクラスもその動作を継承します。
ここで重要な点をまとめます。
- モジュール化を行う場合、モジュールが読み込まれて初めて View がリージョンに注入されます(MedicineModule ビューの読み込み順序を参照)
- ContentControl コントロールは Content が 1 つしか表示できないため、そのリージョンでは Activate および Deactivate メソッドを使用して表示する View を制御します。この動作は ContentControlRegionAdapter アダプターによって制御されます
- ItemsControl コントロールとそのサブコントロールはコレクションビューを表示するため、デフォルトではすべてのコレクションビューがアクティブになります。この場合、Activate および Deactivate メソッドでは制御できません(エラーが発生します)。代わりに Add および Remove を使用して表示する View を制御します。この動作は ItemsControlRegionAdapter アダプターによって制御されます
- Selector コントロールは ItemsControl から継承しているため、その SelectorRegionAdapter アダプターと ItemsControlRegionAdapter アダプターは本質的に同じです
- IActiveAware インターフェースを実装することで、ビューのアクティブ状態を監視できます
4. カスタムリージョンアダプター
リージョンマネージャーのモデル図の説明で述べたように、Prism には 3 つのデフォルトのリージョンアダプター(ItemsControlRegionAdapter、ContentControlRegionAdapter、SelectorRegionAdapter)があり、カスタムリージョンアダプターもサポートされています。ここでは、実際にカスタムアダプターを作成してみましょう。
4.1. カスタムアダプターの作成
新しいクラス UniformGridRegionAdapter.cs を作成します。
public class UniformGridRegionAdapter : RegionAdapterBase<UniformGrid>
{
public UniformGridRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory) : base(regionBehaviorFactory)
{
}
protected override void Adapt(IRegion region, UniformGrid regionTarget)
{
region.Views.CollectionChanged += (s, e) =>
{
if (e.Action==System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
foreach (FrameworkElement element in e.NewItems)
{
regionTarget.Children.Add(element);
}
}
};
}
protected override IRegion CreateRegion()
{
return new AllActiveRegion();
}
}
4.2. マッピングの登録
App.cs:
protected override void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings)
{
base.ConfigureRegionAdapterMappings(regionAdapterMappings);
//UniformGrid コントロールのアダプターマッピングを登録
regionAdapterMappings.RegisterMapping(typeof(UniformGrid),Container.Resolve<UniformGridRegionAdapter>());
}
4.3. コントロールのリージョン作成
MainWindow.xaml:
<UniformGrid
Margin="5"
prism:RegionManager.RegionName="UniformContentRegion"
Columns="2"
>
<button
Content="ActivePaientList"
Margin="5"
Command="{Binding ActivePaientListCommand}"
/>
<button
Content="DeactivePaientList"
Margin="5"
Command="{Binding DeactivePaientListCommand}"
/>
<button
Content="ActiveMedicineList"
Margin="5"
Command="{Binding ActiveMedicineListCommand}"
/>
<button
Content="DeactiveMedicineList"
Margin="5"
Command="{Binding DeactiveMedicineListCommand}"
/>
</UniformGrid>
4.4. リージョンへの View 注入
ここでは ViewInjection 方式を使用します。
MainWindowViewModel.cs:
void ExecuteLoadingCommand()
{
_regionManager = CommonServiceLocator.ServiceLocator.Current.GetInstance<IRegionManager>();
var uniformContentRegion = _regionManager.Regions["UniformContentRegion"];
var regionAdapterView1 = CommonServiceLocator.ServiceLocator.Current.GetInstance<RegionAdapterView1>();
uniformContentRegion.Add(regionAdapterView1);
var regionAdapterView2 = CommonServiceLocator.ServiceLocator.Current.GetInstance<RegionAdapterView2>();
uniformContentRegion.Add(regionAdapterView2);
}
実行結果は次の通りです。

UniformGrid 用のリージョンアダプターを作成して登録したことで、UniformGrid コントロールにもリージョンを作成し、View を注入して表示できるようになりました。このリージョンアダプターがないと、エラーが発生します。次回の記事では、リージョンベースの Prism ナビゲーションシステムについて説明します。
5. ソースコード
最後に、デモ全体のソースコードを添付します:PrismDemo ソースコード