#tl;dr
Regionを設定する際にRegionManager.csのRegionManager添付プロパティも一緒に指定しましょう。
#そもそも
Prism.Wpfで子WindowでRegionを使う方法やPopupWindowActionで出したWindowでRegionを使用するで紹介されているんですけど、PrismのRegion機能ってメインウィンドウから新しいウィンドウをshowして、そのウィンドウ内でRegionをいつも通り設定しても正常に動作しないんですよね。
そのための手法として上記が紹介されているんですけどもう少しシンプルにならないかなぁと調査した結果、RegionManager.csのRegionManager添付プロパティを指定すれば良さそうなのでその内容について投稿します。
#環境
Prism: 7.1.0.431
.NET Framework 4.7.2
#関連プロジェクト
PopupWindowActionSample(自作動作確認用)
Prism
#構成
こんな感じでメインウィンドウからPopupWindowActionを使用してカスタムウィンドウとしてPopupViewを呼び出します。PopupViewでPopupRegionを設定してSubaViewとSubbViewを切り替えれるか確認します。
#RegionManagerのRegionManager添付プロパティとは
RegionManagerから抜粋します。
/// <summary>
/// Identifies the RegionManager attached property.
/// </summary>
/// <remarks>
/// When a control has both the <see cref="RegionNameProperty"/> and
/// <see cref="RegionManagerProperty"/> attached properties set to
/// a value different than <see langword="null" /> and there is a
/// <see cref="IRegionAdapter"/> mapping registered for the control, it
/// will create and adapt a new region for that control, and register it
/// in the <see cref="IRegionManager"/> with the specified region name.
/// </remarks>
public static readonly DependencyProperty RegionManagerProperty =
DependencyProperty.RegisterAttached("RegionManager", typeof(IRegionManager), typeof(RegionManager), null);
以下、google翻訳様です。
コントロールに、RegionNamePropertyとRegionManagerPropertyの両方の添付プロパティがnull以外の値に設定されていて、そのコントロールにIRegionAdapterマッピングが登録されている場合、 そのコントロール用に新しいリージョンを作成して適応させ、指定されたリージョン名でIRegionManagerに登録します。
自分的には両方指定したRegionの中はRegionManagerPropertyで指定したRegionManagerで管理されるよ、って認識です。
なのでメインウィンドウでもRegionを別管理したい領域がある場合(毎回生成するようなViewでRegion指定してる時etc)はこの添付プロパティを指定すれば良いのかなと思います。
#RegionManager添付プロパティ指定例
<UserControl x:Class="PopupWindowActionSample.Views.PopupView"
省略
xmlns:local="clr-namespace:PopupWindowActionSample.Views"
xmlns:prism="http://prismlibrary.com/"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
prism:ViewModelLocator.AutoWireViewModel="True">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding LoadedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<Grid>
<ContentControl prism:RegionManager.RegionName="PopupRegion" prism:RegionManager.RegionManager="{Binding PopupRegionManager.Value}" />
</Grid>
</UserControl>
IRegionManagerのインスタンスを指定して上げれば良いのでバインディングしてViewModelから指定するようにしました。
#どこから新しいRegionManagerを取ってくる?
以下のパターンを考えました。
1.PopupViewModelのコンストラクタで生成
2.ContentViewModelからのリクエスト時に生成してPopupViewに渡す
どっちでも良いような気もしますがリクエスト時に毎回設定するのだるいので1.にしました。
public PopupViewModel(IRegionManager _regionManager)
{
PopupRegionManager.Value = _regionManager.CreateRegionManager();
LoadedCommand = new DelegateCommand(Loaded);
}
コンストラクタインジェクションでメインウィンドウの(デフォルトの)RegionManagerを注入してCreateRegionManagerメソッドで新しいRegionManagerを生成してます。
それからLoadedイベントをフックしてRequestNavigateしてます。
#Region内のViewは毎回どのRegionManager使ってるか意識しないといけないの?
Prism.WpfでRequestNavigateと戻る、進む(IRegionNavigationService, IRegionNavigationJournal)で紹介したことあるんですけど、
OnNavigatedToで取得できるIRegionNavigationServiceを使用すればRegionManagerを意識することなく遷移できます(所属しているRegion内に限る)。
// OnNavigatedToで取得したものを保持.
private IRegionNavigationService RegionNavigationService { get; set; }
// この画面から別の画面に遷移するためのトリガー.
public DelegateCommand GoToSubBCommand { get; }
// コンストラクタでGoToSubBCommandの設定.RegionNavigationServiceを使用すればRegionManagerやRegionNameを意識しなくて良い.
public SubaViewModel() => GoToSubBCommand = new DelegateCommand(() => RegionNavigationService.RequestNavigate(nameof(SubbView)));
// OnNavigatedToで手に入るIRegionNavigationServiceを保持.
public void OnNavigatedTo(NavigationContext navigationContext) => RegionNavigationService = navigationContext.NavigationService;
#使い終わったRegionManagerは解放しないといけないの?
IRegionManagerにもその実装であるRegionManagerにもIDisposeはついてないんですよね。
だからいらないとは思うんですけど自分はとりあえず使い終わったら登録されているRegionをRemoveするようにしようかと思います。
var list = n.PopupRegionManager.Regions.Select(r => r.Name).ToList();
var ret = list.Aggregate(true, (b, name) => b && n.PopupRegionManager.Regions.Remove(name));
if (ret == false)
System.Windows.MessageBox.Show("IRegionのRemoveに失敗");
#PopupWindowActionで呼び出したCustomWindowって右上の閉じるボタン押されたらClosedイベント検知できないんだけど
↑で記載したRegionManagerの後始末をどこでするか?という話なんですけど、
自分の認識だとこの項のタイトル通りなんですけどフック出来たりするんですかね?
右上の閉じるボタンを非表示にするとかPopupWindowActionで呼ばれているDefaultWindowを変更とかいくつか方法はあるとは思うんですけど自分は呼び出し元にRegionManagerを渡して呼び出し元でRemoveするようにしてみました。
// InteractionRequest用に入れ物クラスを用意してその中にIRegionManager格納場所を用意.
public class PopupNotification : INotification
{
public IRegionManager PopupRegionManager { get; set; }
public string Title { get; set; } = "PopupWindow";
public object Content { get; set; }
}
// リクエストをRaiseしてウィンドウが閉じられたときの挙動を設定.
public ContentViewModel() => PopupWindowCommand = new DelegateCommand(() => PopupWindowRequest.Raise(new PopupNotification(), PopupWindowRequestCallback));
private void PopupWindowRequestCallback(PopupNotification n)
{
var list = n.PopupRegionManager.Regions.Select(r => r.Name).ToList();
var ret = list.Aggregate(true, (b, name) => b && n.PopupRegionManager.Regions.Remove(name));
if (ret == false)
System.Windows.MessageBox.Show("IRegionのRemoveに失敗");
}
public class PopupViewModel : IInteractionRequestAware
{
public INotification Notification { get; set; }
public Action FinishInteraction { get; set; }
public DelegateCommand LoadedCommand { get; }
public PopupViewModel(IRegionManager _regionManager)
{
LoadedCommand = new DelegateCommand(Loaded);
}
// PopupWindowActionで指定したViewのLoaded時に入れ物クラスに詰めておく.
// Notificationはコンストラクタの時点ではまだ注入されていなかった.
private void Loaded()
{
if (Notification is PopupNotification n)
n.PopupRegionManager = PopupRegionManager.Value;
}
}
結局呼び出し元で後始末するんならRegionManagerの新しいインスタンスも呼び出し元で詰めても良いような気がしました。
そもそもRegionManagerの後始末が要らないならやらなくて良い処理だと思いますけど。
#まとめ
別窓開いても(Prismデフォルトの機能だけで)Region管理できますよってお話でした。
結局答えは元のソースに書いてあるんですねぇ。
認識違い等あればご指摘お願いします。