かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Prism.Wpfで子WindowでRegionを使う方法

アップデート

もうちょっと頑張ったサンプルをGitHubに公開しました。

github.com

古い内容

PopupWindowActionで表示するWindow内で画面遷移したい!とかいうことがあると使えないんですよね。ということで解決策として、RegionManagerを新しく作って、それを新規のWindowに割り当てるという方法があります。今回はその方法のサンプルを作ってみました。

github.com

MainWindowとSubWindowがあり、MainWindowでボタンを押すとSubWindowがモーダルで表示されて、SubWindow内で画面遷移を行ったうえで画面を閉じるという流れになっています。

RegionManagerを割り当てつつ画面をモーダル表示するアクション

今回は、PrismのPopupWindowActionを使うのではなく、自前のWindow表示用Actionを作りました。こんな感じでRegionManagerを新しく作ってWindowに割り当てています。

using Prism.Regions;
using System;
using System.Windows;
using System.Windows.Interactivity;

namespace WpfApplication13.Commons
{
    public class ShowModalWindowAction : TriggerAction<DependencyObject>
    {
        public Type WindowType
        {
            get { return (Type)GetValue(WindowTypeProperty); }
            set { SetValue(WindowTypeProperty, value); }
        }

        // Using a DependencyProperty as the backing store for WindowType.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty WindowTypeProperty =
            DependencyProperty.Register("WindowType", typeof(Type), typeof(ShowModalWindowAction), new PropertyMetadata(typeof(Window)));

        protected override void Invoke(object parameter)
        {
            var currentRegionManager = RegionManager.GetRegionManager(Window.GetWindow(this.AssociatedObject));
            var w = (Window)Activator.CreateInstance(this.WindowType);
            RegionManager.SetRegionManager(w, currentRegionManager.CreateRegionManager());
            w.ShowDialog();
        }
    }
}

このActionを使うことで新たなRegionManagerを割り当てたWindowが表示されます。もうちょっと欲を言うと、PrismのPopupWindowActionと同じようにViewModel等にNotificationを渡してやる機能とかを追加してもいいかもしれません。

使い方

使い方はInteractionRequestTrigger等のTriggerの下に置くだけです。

<UserControl 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:WpfApplication13.Views"
             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
             xmlns:Commons="clr-namespace:WpfApplication13.Commons"
             xmlns:Prism="http://prismlibrary.com/"
             x:Class="WpfApplication13.Views.ViewA"
             mc:Ignorable="d"
             Prism:ViewModelLocator.AutoWireViewModel="True"
             d:DesignHeight="300"
             d:DesignWidth="300">
    <i:Interaction.Triggers>
        <Prism:InteractionRequestTrigger SourceObject="{Binding ShowWindowRequest}">
            <Commons:ShowModalWindowAction WindowType="{x:Type local:SubWindow}" />
        </Prism:InteractionRequestTrigger>
    </i:Interaction.Triggers>
    <Grid>
        <Button Content="ShowWindow"
                Command="{Binding ShowWindowCommand}"/>
    </Grid>
</UserControl>

指定したタイプのWindowを表示します。

SubWindow

SubWindowは、普通にRegionを切っているだけのWindowです。

<Window x:Class="WpfApplication13.Views.SubWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication13.Views"
        xmlns:Prism="http://prismlibrary.com/"
        mc:Ignorable="d"
        Title="SubWindow"
        Height="300"
        Width="300">
    <Grid>
        <ContentControl Prism:RegionManager.RegionName="ChildMain" />
    </Grid>
</Window>

SubWindow内の初期のViewの表示

これがいまいちだと思ってるのですが、SubWindowのLoadedイベントを購読して、そこで実施しています。もうちょっと改善したいなと思っています。まぁでも、これくらいが手軽で分かりやすいといえばわかりやすいのでバランスが難しいところです。

using Prism.Regions;
using System.Windows;

namespace WpfApplication13.Views
{
    /// <summary>
    /// SubWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class SubWindow : Window
    {
        public SubWindow()
        {
            InitializeComponent();
            this.Loaded += (_, __) =>
            {
                var rm = RegionManager.GetRegionManager(this);
                rm.RequestNavigate("ChildMain", "ChildViewA");
            };
        }
    }
}

SubWindowを閉じる

SubWindowを閉じるのは、Windowを閉じる専用のActionを定義してそれでやるようにしました。以下のような簡単なActionです。

using System.Windows;
using System.Windows.Interactivity;

namespace WpfApplication13.Commons
{
    public class WindowCloseAction : TriggerAction<DependencyObject>
    {
        protected override void Invoke(object parameter)
        {
            Window.GetWindow(this.AssociatedObject)?.Close();
        }
    }
}

これをInteractionRequestTriggerと組み合わせてChildViewBで使っています。

<UserControl 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:WpfApplication13.Views"
             xmlns:Prism="http://prismlibrary.com/"
             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
             xmlns:Commons="clr-namespace:WpfApplication13.Commons"
             x:Class="WpfApplication13.Views.ChildViewB"
             Prism:ViewModelLocator.AutoWireViewModel="True"
             mc:Ignorable="d"
             d:DesignHeight="300"
             d:DesignWidth="300">
    <i:Interaction.Triggers>
        <Prism:InteractionRequestTrigger SourceObject="{Binding WindowCloseRequest}">
            <Commons:WindowCloseAction />
        </Prism:InteractionRequestTrigger>
    </i:Interaction.Triggers>
    <Grid>
        <Button Content="Close" 
                Command="{Binding WindowCloseCommand}"/>
    </Grid>
</UserControl>

SubWindowのRegionManagerをVMから使う

これは少しトリッキーです。ViewModelに以下のようなIRegionManagerのプロパティを定義しています。

using Prism.Commands;
using Prism.Mvvm;
using Prism.Regions;

namespace WpfApplication13.ViewModels
{
    public class ChildViewAViewModel : BindableBase
    {
        public IRegionManager RegionManager { get; set; }

        public DelegateCommand NavigateCommand { get; }

        public ChildViewAViewModel()
        {
            this.NavigateCommand = new DelegateCommand(() =>
            {
                this.RegionManager.RequestNavigate("ChildMain", "ChildViewB");
            });
        }
    }
}

それをViewのXAMLからBindingのOneWayToSourceでインジェクションしています。

<UserControl x:Class="WpfApplication13.Views.ChildViewA"
             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:WpfApplication13.Views"
             xmlns:Prism="http://prismlibrary.com/"
             Prism:ViewModelLocator.AutoWireViewModel="True"
             Prism:RegionManager.RegionManager="{Binding RegionManager, Mode=OneWayToSource}"
             mc:Ignorable="d"
             d:DesignHeight="300"
             d:DesignWidth="300">
    <StackPanel>
        <TextBlock Text="Child" />
        <Button Content="Nav" 
                Command="{Binding NavigateCommand}" />
    </StackPanel>
</UserControl>

これは、気づかないと「???」という感じです。Unityが管理してるのはMainWindowに紐づいてるRegionManagerだと思うので、SubWindowに紐づくやつは、こうやって設定しています。Unityのコンテナをネストするような仕組みを作りこめばDIでいい感じに出来なくはないけど、複雑になってきちゃう。

まとめ

Prismで複数WindowでRegionを使うのは難しいです。シンプルに1画面に収めてポップアップは補助的という感じのアプリを作るのが作りやすいですね。(複雑な子画面も今回のようにやってできないわけではないけど厄介)