PowerShell+WPFでの変更通知実装

PowerShellでWPFを使うとき、変更通知つきのプロパティにBindingする方法です。

結論

ラップしたC#を書くのがオススメ

難点1:そもそもPowerShellでプロパティが書けない

正確に言うと、普通の書き方ではgetter/setterを書けません。

以下のようにいわゆるフィールドのように書くと、それはプロパティ扱いになりますが、

肝心の処理はかけず、自動プロパティのようになります。

class ViewModel{
    [String] $SelectedItem
}

もちろん変更通知を書けないので、これは使えません

難点2:無理やり付け加えると、Bindingできない

Add-MemberとScriptPropertyを組み合わせて使うと、getter/setterつきのプロパティを書くことができますが、Add-Memberで追加したプロパティにはBindingできません。
using namespace System.ComponentModel
using namespace System.Collections.ObjectModel

class ViewModel: INotifyPropertyChanged
{
  hidden [string]$m_slectedItem
  [System.Collections.ArrayList]$PropertyChanged = @()
  
  [void] add_PropertyChanged([PropertyChangedEventHandler]$handler)
  {
    $this.PropertyChanged.Add($handler)
  }
  [void] remove_PropertyChanged([PropertyChangedEventHandler]$handler)
  {
    $this.PropertyChanged.Remove($handler)
  }

  [void] OnPropertyChanged([string]$propname)
  {
    if ($this.PropertyChanged)
    {
      $evargs = [PropertyChangedEventArgs]::new($propname)
      $this.PropertyChanged.Invoke($this, $evargs)
    }
  }

  [ObservableCollection[string]] $Items = [ObservableCollection[String]]::new()

  MainViewModel()
  {
    #SelectedItemにBindingしても無反応
    $vm = $this
    $this | Add-Member -Name "SelectedItem" -MemberType ScriptProperty `
        -Value {
            return $vm.m_slectedItem
        }.GetNewClosure() `
        -SecondValue {
            param($value)
            $vm.m_slectedItem = $value
            $vm.OnPropertyChanged("SelectedItem")
        }.GetNewClosure()

    $this.Items.Add("Apple")
    $this.Items.Add("Banana")
    $this.Items.Add("Cherry")
  }
}

仕方ないのでC#書く

どうしようもないので、C#で変更通知を実装した、なんちゃってReactivePropertyを書きます。

PowerShellで読み込めるC#のバージョンは古いので、

get =>_value とか nameof(Value) とか PropertyChanged?.Invoke とか使えないので要注意。

using System;
using System.ComponentModel;
using System.Windows.Input;

public class ReactiveProperty<T> : INotifyPropertyChanged
{
    private T _value;
    public T Value
    {
        get { return _value; }
        set {
            if (!Equals(_value, value)){
                _value = value;
                OnPropertyChanged("Value");
            }
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        if(PropertyChanged != null){
            PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

一応、これをモジュールで読み込みます

Split-Path $MyInvocation.MyCommand.Path -Parent | Set-Location
Add-Type -Path ".\ReactiveProperty.cs"

これをusing moduleすれば使えます。