通常、UserControl から ContentRendered イベントを使いたいシーンはあまり無いと思いますが、例外になるケースがありました。
Prism の DialogWindow
を利用した場合、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 が登録されるのは、コントロールの描画を完了したタイミング ContentRendered
や LayoutUpdated
のタイミングをみて、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 にサンプルコードを公開しています。