PowerShellでWPFを使う 

# はじめに
WPFはC#やVisual Basicでの実装を前提とされている面がありますが、Powshellでの実装もそれなりに対応しています。
自由に言語を選ぶならC#でしょうけど、C#よりPowerShellに触れる機会の多い職種の人もたくさんいますし、プロジェクトによってはC#の開発環境が許可されないなんてことも。最近のWindowsだと、PowerShell 5はデフォルトで入っているんですよね。
ということでPoworShellでのWPFアプリの作成ができると案外便利ですし、割と普通に作れます。

#最初の表示
まずは最低限の表示から。

```PowerShell:wpf-1.ps1
using namespace System.Xml

Add-Type -AssemblyName PresentationFramework
[xml]$xaml = '
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="100" Height="100">
Hello WPF!!
</Window>'

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

$window.ShowDialog()
```

![wpf-1-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34050/16e638af-c77f-0618-8dd2-07bf1a83ec2a.jpeg)

#xamlファイル
XAMLは別ファイルにしておいたほうがいいかもしれません。
方法は簡単です。```Get-Content```で呼び出すだけ。

```xml:wpf-2.xaml
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="100" Height="100">
Hello WPF!!
</Window>
```
```PowerShell:wpf-2.ps1
using namespace System.Xml

Add-Type -AssemblyName PresentationFramework
[xml]$xaml = Get-Content .\wpf-2.xaml
$nodeReader = (New-Object XmlNodeReader $xaml)
$window = [Windows.Markup.XamlReader]::Load($nodeReader)

$window.ShowDialog()
```

![wpf-2-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34050/16e638af-c77f-0618-8dd2-07bf1a83ec2a.jpeg)

#コントロールの取得
```PowerShell
$label=$window.FindName("label")
```
上のようにFindNameメソッドを使えば、コントロールを取得することができ、それに対する操作もできます。

```xml:wpf-3.xaml
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="100" Height="100">
    <StackPanel>
        <Label x:Name="label"/>
    </StackPanel>
</Window>
```
```PowerShell:wpf-3.ps1
using namespace System.Xml

Add-Type -AssemblyName PresentationFramework
[xml]$xaml = Get-Content .\wpf-3.xaml
$nodeReader = (New-Object XmlNodeReader $xaml)
$window = [Windows.Markup.XamlReader]::Load($nodeReader)

$label=$window.FindName("label")
$label.Content = "Hello"

$window.ShowDialog()
```
![wpf-3-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34050/41224963-1767-63c7-cfc8-4c03b7c1003c.jpeg)
#イベント
イベントは、「add_イベント名」というメソッドにスクリプトブロックを引き渡すことによって登録できます。

```PowerShell
$button.add_Click({$label.Content++})
```
これを利用して以下のように書くことができます。

```xml:wpf-4.xaml
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="100" Height="100">
    <StackPanel>
        <Label x:Name="label"/>
        <Button x:Name="button">Click!</Button>
    </StackPanel>
</Window>
```

```PowerShell:wpf-4.ps1
Add-Type -AssemblyName PresentationFramework
[xml]$xaml = Get-Content .\wpf-4.xaml
$nodeReader = (New-Object XmlNodeReader $xaml)
$window = [Windows.Markup.XamlReader]::Load($nodeReader)
$label=$window.FindName("label")
$button=$window.FindName("button")
$label.Content = 1
$button.add_Click({$label.Content++})
$window.ShowDialog()
```
![wpf-4-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34050/6494ad93-7325-cfa6-1063-e616da3eda65.jpeg)
↓クリック!
![wpf-4-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34050/5bed972b-569e-18ca-58a4-40b1f1bf145e.jpeg)

#データバインド
データバインドも普通にできます。
PSCustomObjectも普通に使えます。

```xml:wpf-4.xaml
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="100" Height="100">
    <StackPanel>
        <TextBlock>姓</TextBlock>
        <TextBlock Text="{Binding LastName}" />
        <TextBlock>名</TextBlock>
        <TextBlock Text="{Binding FirstName}" />
    </StackPanel>
</Window>
```
```PowerShell:wpf-5.ps1
Add-Type -AssemblyName PresentationFramework
[xml]$xaml = Get-Content .\wpf-5.xaml
$nodeReader = (New-Object XmlNodeReader $xaml)
$window = [Windows.Markup.XamlReader]::Load($nodeReader)
$window.DataContext = [PSCustomObject]@{
    FirstName = "太郎";
    LastName = "佐藤"
}
$window.ShowDialog()
```
PowerShellのクラスを使ってもバインドできます。

```PowerShell:wpf-5-2.ps1
using namespace System.Xml

Add-Type -AssemblyName PresentationFramework
[xml]$xaml = Get-Content .\wpf-5.xaml
$nodeReader = (New-Object XmlNodeReader $xaml)
$window = [Windows.Markup.XamlReader]::Load($nodeReader)

class PSClass
{
    [string]$FirstName
    [string]$LastName
}

$obj = New-Object PSClass
$obj.FirstName = "太郎"
$obj.LastName = "佐藤"

$window.DataContext = $obj
$window.ShowDialog()
```
![wpf-5-1.jpg](https://qiita-image-store.s3.ap-northeast-
1.amazonaws.com/0/34050/9dabbc15-15fe-8b63-0022-e19828b92a62.jpeg)
#INotifyPropertyChanged
WPFでアプリケーションを実装するにはINotifyPropertyChangedインターフェースを実装することが重要になってきます。
PowerShellでそれを簡単に実現するためにはExpandoObjectを利用するという方法があります。
生成する関数を作っておくとよいでしょう。

```PowerShell:New-ExpandoObject
function New-ExpandoObject([PSCustomObject]$source){
    $source = [PSCustomObject]$source
    $ret = New-Object ExpandoObject
    $source | Get-Member | ?{$_.MemberType -eq "NoteProperty"} | %{$ret.($_.Name) = $source.($_.Name)}
    return ,$ret
}
```
ExpandoObjectはINotifyPropertyChangedを実装しているため、データバインドを使ったデータの連携ができます。

```xml:wpf-6.xaml
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="100" Height="120">
    <StackPanel Orientation="Horizontal">
        <StackPanel>
            <TextBlock>姓</TextBlock>
            <TextBox Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}" />
            <TextBlock>名</TextBlock>
            <TextBox Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" />
        </StackPanel>
        <StackPanel>
            <TextBlock>姓</TextBlock>
            <TextBox Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}" />
            <TextBlock>名</TextBlock>
            <TextBox Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" />
        </StackPanel>
    </StackPanel>
</Window>
```
```PowerShell:wpf-6.ps1
using namespace System.Xml
using namespace System.Dynamic

function New-ExpandoObject([PSCustomObject]$source){
    $source = [PSCustomObject]$source
    $ret = New-Object ExpandoObject
    $source | Get-Member | ?{$_.MemberType -eq "NoteProperty"} | %{$ret.($_.Name) = $source.($_.Name)}
    return ,$ret
}

Add-Type -AssemblyName PresentationFramework
[xml]$xaml = Get-Content .\wpf-6.xaml
$nodeReader = (New-Object XmlNodeReader $xaml)
$window = [Windows.Markup.XamlReader]::Load($nodeReader)
$window.DataContext = New-ExpandoObject @{
    FirstName = "太郎";
    LastName = "佐藤"
}
$window.ShowDialog()
```
![wpf-6-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34050/4fc41dcc-c8e1-9d11-5e82-e28e4e8014e7.jpeg)![wpf-6-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34050/049282e0-eebc-ecaf-4ee7-a24541cac2fb.jpeg)
#INotifyCollectionChanged
ObservableCollectionは普通に使えますね。

```xml:wpf-7.xaml
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="200" Height="200">
    <DockPanel LastChildFill="False">
        <Button DockPanel.Dock="Bottom" x:Name="button">追加</Button>
        <DataGrid ItemsSource="{Binding}" CanUserAddRows="False">
            <DataGrid.Columns>
                <DataGridTextColumn  Header="姓" Binding="{Binding LastName, UpdateSourceTrigger=PropertyChanged}"/>
                <DataGridTextColumn  Header="名" Binding="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}"/>
            </DataGrid.Columns>
        </DataGrid>
        <DataGrid ItemsSource="{Binding}" CanUserAddRows="False"> 
            <DataGrid.Columns>
                <DataGridTextColumn  Header="姓" Binding="{Binding LastName, UpdateSourceTrigger=PropertyChanged}"/>
                <DataGridTextColumn  Header="名" Binding="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}"/>
            </DataGrid.Columns>
        </DataGrid>
    </DockPanel>
</Window>
```
```PowerShell:wpf-7.ps1
using namespace System.Xml
using namespace System.Dynamic
using namespace System.Collections.ObjectModel

function New-ExpandoObject([PSCustomObject]$source){
    $source = [PSCustomObject]$source
    $ret = New-Object ExpandoObject
    $source | Get-Member | ?{$_.MemberType -eq "NoteProperty"} | %{$ret.($_.Name) = $source.($_.Name)}
    return ,$ret
}

Add-Type -AssemblyName PresentationFramework
[xml]$xaml = Get-Content .\wpf-7.xaml
$nodeReader = (New-Object XmlNodeReader $xaml)
$window = [Windows.Markup.XamlReader]::Load($nodeReader)
$button = $window.FindName("button")
$collection = New-Object ObservableCollection[ExpandoObject]
$window.DataContext = $collection
$button.add_Click({
    $collection.Add((New-ExpandoObject @{FirstName="";LastName=""}))
})
$window.ShowDialog()
```
![wpf-7-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34050/cb75a778-3833-d405-165d-3b32326994ca.jpeg)
![wpf-7-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34050/30c83c93-d254-f4d8-e25d-f3b6d8dfab08.jpeg)
![wpf-7-3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34050/0945cc02-47c4-231e-f861-9d9b64f5bb3f.jpeg)

#C#でのクラス定義
PowerShellではC#でのクラスも定義できます。文字列でC#のコードを使うという大胆な記法。
C#との連携を強く考えて設計されているWPFは実装しやすくなるとは思いますが、PowerShell内にC#のコードが多数埋め込まれたコードは大変わかりにくくなり、PowerShellらしい書き方もできなくなるため、私はなるべく使わないに越したことはないと思います。
これを使えばINotifyPropertyChangedなども普通に実装できます。

```xml:wpf-8.xaml
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="100" Height="120">
    <StackPanel Orientation="Horizontal">
        <StackPanel>
            <TextBlock>姓</TextBlock>
            <TextBox Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}" />
            <TextBlock>名</TextBlock>
            <TextBox Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" />
        </StackPanel>
        <StackPanel>
            <TextBlock>姓</TextBlock>
            <TextBox Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}" />
            <TextBlock>名</TextBlock>
            <TextBox Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" />
        </StackPanel>
    </StackPanel>
</Window>
```
```PowerShell:wpf-8.ps1
using namespace System.Xml

Add-Type -AssemblyName PresentationFramework
Add-Type '
using System.ComponentModel;
using System.Runtime.CompilerServices;
public class Name : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    void Set<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
    {
{
        if(!field.Equals(value))
        {
            field = value;
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
    }
    string firstName = "";
    public string FirstName
    {
        get{return firstName;}
        set{Set(ref firstName, value);}
    }
    string lastName = "";
    public string LastName
    {
        get{return lastName;}
        set{Set(ref lastName, value);}
    }
}
'
[xml]$xaml = Get-Content .\wpf-8.xaml
$nodeReader = (New-Object XmlNodeReader $xaml)
$window = [Windows.Markup.XamlReader]::Load($nodeReader)
$name = New-Object Name
$name.LastName = "佐藤"
$name.FirstName = "太郎"
$window.DataContext = $name

$window.ShowDialog()
```
![wpf-8-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34050/0ebba753-20a1-e03f-7f70-e40910b54ff7.jpeg)
![wpf-8-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34050/ca5e8c28-cbc1-04ec-4de4-baa710815479.jpeg)