sh1’s diary

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

WPF UserControl クラスから ContentRendered イベントをバインドする

通常、UserControl から ContentRendered イベントを使いたいシーンはあまり無いと思いますが、例外になるケースがありました。

PrismDialogWindow を利用した場合、UserControl を簡単に子ウィンドウとして表示することができるのですが、UserControl には ContentRendered イベントがありません。(UserControl の親 Window で発生するイベントです)

問題の例

<UserControl x:Class="JSL.Modules.ENV.Views.MenuWindow"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:sys="clr-namespace:System;assembly=mscorlib"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:prism="http://prismlibrary.com/">
    <prism:Dialog.WindowStyle>
        <Style TargetType="Window">
            <Setter Property="prism:Dialog.WindowStartupLocation" Value="CenterOwner" />
            <Setter Property="WindowStyle" Value="ToolWindow" />
            <Setter Property="ResizeMode" Value="NoResize"/>
            <Setter Property="ShowInTaskbar" Value="False"/>
            <Setter Property="SizeToContent" Value="WidthAndHeight"/>
        </Style>
    </prism:Dialog.WindowStyle>
</UserControl>

Prism の子ウィンドウは、RegionManager が通常のままだと正しく動作しない、という問題を抱えています。(または、RegionManager はそういう仕様です)

なので、RegionManager はコンストラクター内で以下のように、新しい RegionManager を生成することになります。

private IRegionManager _RegionManager;
public IRegionManager ChildRegionManager => _RegionManager;

public ClassConstractor(IRegionManager regionManager)
{
    `_RegionManager = regionManager.CreateRegionManager();`
}

DI で受け取った RegionManager をそのまま利用すると、親ウィンドウ内の領域を管理するマネージャーを受け取っているため、子ウィンドウの VM コンストラクターから、親ウィンドウ内の領域を操作してしまう。

ここで新しく生成した RegionManager は、子ウィンドウ内に用意した領域を認識します。以下のようなコントロールを追加して、RegionName を管理する RegionManager を指定しています。

<ContentControl prism:RegionManager.RegionName="SampleRegion"
    prism:RegionManager.RegionManager="{Binding ChildRegionManager}"/>

通常であれば、子ウィンドウの表示と共に RegionName の領域に適当なコントロールの表示を指示します。

_RegionManager.RequestNavigate("SampleRegion", "登録コントロール名");

しかし、コンストラクター実行のタイミングで RequestNavigate を実行しても、期待したコントロールは表示されません。ちなみに、UserControl の Loaded のタイミングでもダメ。

つまり、子ウィンドウを表示したとき SampleRegion の領域を初期化しないと空白が表示されてしまう場合、どのタイミングで(表示する画面を RequestNavigate する)設定するのかが問題です。

解答の例としては、RegionManager に RegionName が登録されるのは、コントロールの描画を完了したタイミング ContentRenderedLayoutUpdated のタイミングをみて、RequestNavigate を実行してみるとよいです。(参考に「WPF Window起動時のイベント発生順位をメモ」を確認しておこう)

そんなわけで、Prism の DialogWindow で Region を利用するためには、一工夫必要になるシーンがあります。一工夫の解決策のひとつとしてContentRendered イベントのタイミングで RequestNavigate を実行する、というものがありますよ、と。

ContentRendered イベントを UserControl から利用する

添付プロパティを使って解決する例。UserControl に ContentRendered.Command を追加します。で、ICommand を使って VM からアクセスできるようにします。

<UserControl x:Class="JSL.Modules.ENV.Views.MenuWindow"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:sys="clr-namespace:System;assembly=mscorlib"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:prism="http://prismlibrary.com/"
             heri_atta:ContentRendered.Command="{Binding ContentRenderedCommand}">
    <prism:Dialog.WindowStyle>
        <Style TargetType="Window">
            <Setter Property="prism:Dialog.WindowStartupLocation" Value="CenterOwner" />
            <Setter Property="WindowStyle" Value="ToolWindow" />
            <Setter Property="ResizeMode" Value="NoResize"/>
            <Setter Property="ShowInTaskbar" Value="False"/>
            <Setter Property="SizeToContent" Value="WidthAndHeight"/>
        </Style>
    </prism:Dialog.WindowStyle>
</>
public class MainWindowViewModel
{
    public ICommand ContentRenderedCommand { get; }

    public MainWindowViewModel()
    {
        ContentRenderedCommand = new Command(() =>
        {
            MessageBox.Show($"{ContentRenderedCommand} is executed.");
        });
    }
}

public class Command : ICommand
{
    private Action _Action;
    public bool CanExecute(object? parameter) => true;
    public event EventHandler? CanExecuteChanged;

    public Command(Action action)
    {
        _Action = action;
    }

    public void Execute(object? parameter)
    {
        _Action?.Invoke();
    }
}

こんな感じで、ContentRendered イベントの発生時に ContentRenderedCommand を呼び出して貰えるような添付プロパティを作ります。

このタイミングだと上記例のような、Prism の RegionManager も RegionName を取得済なので、RequestNavigate できるようになる、という考えです。(実際、有効でした)

ContentRendered.Command の実装例

次のような感じでどうでしょうか。すこし悩んだのは、イベントを実行する DependencyObject が UserControl ではなくて親にあたる Window です。なので、イベント発生時に Window のインスタンス sender から GetCommand をしても、Binding している ICommand を取得できないのです。

そんなわけで、ContentRendered イベントに対応するメソッドは、ローカル関数を使って匿名メソッドを実装して controlインスタンスをぬるっと取得しています。(クロージャ:closure)

クロージャの書き方は、気を付けて利用する必要があります。必要ないならやらない方がパフォーマンス的にもよいので、明示するために静的ローカル関数という書き方も用意されています。

using System.Windows.Input;
using System.Windows;
using System.Diagnostics;
using System.Windows.Controls;

/// <summary>
/// <see cref="ContentRendered"/> クラスは、<see cref="Window.ContentRendered"/> イベントを <see cref="UserControl"/> クラスで利用するための添付プロパティを定義するクラスです。
/// </summary>
public class ContentRendered
{
    private static readonly DependencyProperty CommandProperty =
        DependencyProperty.RegisterAttached(
            "Command",
            typeof(ICommand),
            typeof(ContentRendered),
            new FrameworkPropertyMetadata(
                null,
                FrameworkPropertyMetadataOptions.None,
                (s, e) =>
                {
                    if (s is UserControl control)
                    {
                        if (control == null)
                        {
                            Debug.WriteLine($"{typeof(ContentRendered)}.{CommandProperty}: control is null.");
                            return;
                        }

                        var window = Window.GetWindow(control);

                        if (window == null)
                        {
                            Debug.WriteLine($"{typeof(ContentRendered)}.{CommandProperty}: parent window is null.");
                            return;
                        }

                        window.ContentRendered += (sender, args) =>
                        {
                            var command = GetCommand(control);

                            command?.Execute(e);
                        };
                    }
                })
            );

    public static ICommand GetCommand(DependencyObject d)
    {
        return (ICommand)d.GetValue(CommandProperty);
    }

    public static void SetCommand(DependencyObject d, ICommand value)
    {
        d.SetValue(CommandProperty, value);
    }
}

サンプル

GitHub にサンプルコードを公開しています。

参考