PowerShell+XAMLでDLLを使わずにC#コードに直接イベントハンドラーを連携させる

PowerShell + WPF(XAML) を使った際に `add_click` 等を使わずにイベントを登録することはできないか以前から気になっていたが、調べた結果実現出来そうだったので調査・検証をしてその結果を本記事に記す。

# 本記事の対象ユーザ
* PowerShell + XAML を使っているがコード内で後からイベントを登録したくない人

# 起稿時の環境
* Windows 10 Pro(x64)

# 実装手法
以下の実装方法について紹介と検証を行う。
動作検証としてボタンのクリックによりメッセージボックスを表示する実装を行う。

1. Window 作成後にイベントハンドラーを後から登録する方法
   これが現在割りと一般的な手法(と思われる)。
1. DLL を使ったイベントハンドラーの連携の紹介
   事前に DLL を作成して、実行時にロードする手法。
1. DLL を使用しないイベントハンドラーの連携
   本記事で調査した手法。

# Window 作成後にイベントハンドラーを後から登録する方法
一般的に利用される手法。

```:構成
sample01\
  + ui01.xaml     (1)
  + script01.ps1  (2)
```

```xml:(1) ui01.xaml
<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window" Height="450" Width="800">
    <StackPanel>
        <Button Content="Click Me" Name="buttonClickeMe" />
    </StackPanel>
</Window>
```
`System.Windows.Window` クラスでウィンドウを作成する。ウィンドウ内にはボタンを設置している。


```powershell:(2) script01.ps1
using namespace System.Xml  # XMLパーサ用
Add-Type -Assembly System.Windows.Forms  # メッセージボックス用
Add-Type -AssemblyName PresentationFramework  # WPF用

[xml]$xamlStr = Get-Content .\ui01.xaml
$nodeReader = (New-Object XmlNodeReader $xamlStr)
$window = [Windows.Markup.XamlReader]::Load($nodeReader)

$window.findName("buttonClickeMe").add_click({
    [System.Windows.Forms.MessageBox]::Show("メッセージ表示", "Test") 
})
$window.ShowDialog()
```

必要なモジュール類をロードして、XAML からウィンドウを作成している。
ウィンドウ内のボタンにイベントを登録しているシンプルな構成となっている。
コードはこちらのサイトを参考に作成([参考](#参考)#1,2,3)。

## 実行結果
![スクリーンショット 2022-05-04 234011.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124578/0023f335-643a-f702-3cb1-f2eac31a9b9f.png)

# DLL を使ったイベントハンドラーの連携の紹介
DLL を使ったケースについては下記サイトを参照するのが良い。
* PowerShellでWPFとかイベントとか ～ 枠組みをC#で書く ～ - ブログ「サイバー少年」
  http://cyberboy6.blog.fc2.com/blog-entry-445.html

# DLL を使用しないイベントハンドラーの連携
本記事の主旨となる情報です。

```:構成
sample02\
  + ui02.xaml     (1)
  + script02.ps1  (2)
```

```xml:(1) ui02.xaml
<WindowsOverview:Window1
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:WindowsOverview="clr-namespace:WindowsOverview;assembly={dllname}"
        >
    <StackPanel>
        <Button Click="Button_Click">Click me</Button>
    </StackPanel>
</WindowsOverview:Window1>
```
`<WindowsOverview:Window1` は C# コードの名前空間とクラス名を指定する。
`xmlns:WindowsOverview="clr-namespace:WindowsOverview;assembly={dllname}"` では名前空間を明示し、`assembly`では本来 DLL 名を指定するがここではスクリプト実行時にコンパイルするメモリ内に構築した静的アセンブリ名を指定する。`{dllname}`はコンパイルするたびに基本的に名前が変わる為、スクリプト内で置換して XmlNodeReader を通す前に埋め込む。

```powershell:(2) script02.ps1
using namespace System.Xml

Add-Type -ReferencedAssemblies System.Xaml,WindowsBase,PresentationCore,PresentationFramework @'
using System.Windows;

namespace WindowsOverview
{
    public partial class Window1 : Window
    {
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("ボタンがクリックされました。");
        }
    }
}
'@

$dllName = ([System.AppDomain]::CurrentDomain.GetAssemblies() |
    Where-Object { $_.DefinedTypes.Name -eq "Window1" }).getName().Name  # 静的なアセンブラ名を取得する
[xml]$xamlStr = (Get-Content .\ui02.xaml) -replace ("{dllname}",$dllName)

$nodeReader = (New-Object XmlNodeReader $xamlStr)
$window = [Windows.Markup.XamlReader]::Load($nodeReader)

$window.ShowDialog()
```
`Add-Type` で C# コードを追加する際に -ReferencedAssemblies で C# 内から利用するライブラリを追加しておく必要がある。
XAML が参照する静的ライブラリ名を取得して、XAMLReaderに読み込ませる前に予め置換して埋め込んで置く。今回 Class 名を Window1 としているため Where-Object でも Window1 でフィルタしているが、Class 名を変更する場合はここも変更する。


## 実行結果
![スクリーンショット 2022-05-05 001402.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124578/da258820-0800-ecd1-f05d-f3de70dca43b.png)
想定した通りに動きました。ウィンドウタイトルを入れ忘れました。

# あとがき
PowerShell で XAML を使った場合、ルートインスタンスとして `<Window` を使う情報が見つかるためこれを変更できないと考えていましたが、「サイバー少年」([参考](#参考)#4)さんのページで Window のサブクラス(別名)を指定する方法を見かけて今回の調査に至りました。その後、[参考](#参考)#5,6の情報を元に無事当初の目的が達成できました。

記事内容について、誤りや補足があればコメントでお知らせいただけると幸いです。

# 参考
記事内で参照しているもの、参照していないが参考とさせていただいたもの。

1. PowerShellでWPFを使う - Qiita
   https://qiita.com/potimarimo/items/1eca0516bd8c690872dc
1. XAML の概要 - WPF .NET | Microsoft Docs
   https://docs.microsoft.com/ja-jp/dotnet/desktop/wpf/xaml/?view=netdesktop-6.0
1. PowerShellをはじめよう　～PowerShell入門～: PowerShellでMessageBoxを使用する
   https://letspowershell.blogspot.com/2015/06/powershellmessagebox.html
1. PowerShellでWPFとかイベントとか ～ 枠組みをC#で書く ～ - ブログ「サイバー少年」
   http://cyberboy6.blog.fc2.com/blog-entry-445.html
1. WPF - XAML, with C# code behind : PowerShell
   https://www.reddit.com/r/PowerShell/comments/9sd0ex/wpf_xaml_with_c_code_behind/
1. c# - Dynamic DataTemplate in a PowerShell WPF app - Stack Overflow
   https://stackoverflow.com/questions/52023418/dynamic-datatemplate-in-a-powershell-wpf-app/
1. PowerShell でロード済みのアセンブリを確認する
   http://www.vwnet.jp/windows/PowerShell/CheckAssemblis.htm