Autodesk Inventor API Hacking (NET環境での別スレッドUI)

# 0. はじめに
[Freeradical](https://www.freeradical.jp)の中の人、[yamarah](https://qiita.com/yamarah)です。
自前ダイアログで条件を設定し、Inventor自体に重い処理をさせる場合に、キャンセルボタンが欲しいですよね。
完全にシングルスレッドにしてしまうと、重い処理を実行中にUIが操作できなくなります。何らかの方法でマルチスレッドにするしかないのですが、どのようにすれば・・・というお話です。

# 1. 前置き
## 1.1. 正攻法
通常のWPFアプリケーションでは、メインスレッドたるUIスレッドは瞬時リターンが原則で、時間のかかる処理は`async`, `await`を使って別スレッドに委託します。
今回の時間がかかる処理は、Inventorが処理します。すると、Inventorが考え込む = UIスレッドが止まる、ためにキャンセルボタンが押せなくなります。

## 1.2. 方針
Inventorは構造解析などの例外を除きシングルスレッドなので、Inventorに重い処理をさせている間はUIスレッドが止まります。これは逃げられないです。
ですので、`await` → `Task.Run()` → `InventorのAPI呼び出し`と繋いでも無意味で、UIがロックします。
仕方が無いので正攻法とは逆の発想で、標準とは別のUIスレッドを作って、そこでダイアログを表示させます。

## 1.3. 本記事のテストコードについて
InventorのAddIn上で実行するのが本来の目的のものの、別スレッドでUIを動かくことが本記事の主題なので、以下の方針でテストコードを書きます。
* InventorのAddInにせず、単独アプリケーションとする
* `Button1`, `Button2`の2つのButtonを持った`MainWindow`をxamlで用意しているものとする
* `Button1`を押すと、新たな`MainWindow`を別スレッドで開く
* `Button2`を押すと、`Button1`で開いた`MainWindow`を閉じる
* `lock`や`SemaphoreSlim`を使いスレッド間で同期を取るべき場面も、今回は省略しています。ですので、このままのコードでは危険です。

# 2. 別スレッドでUI起動
この記事を参考にして、まずは起動側のコードを書きます。
(参考記事は「複数UIスレッドは止めとけ」って内容なのですけど)

https://qiita.com/okazuki/items/698ec4d45c8286fedac1

```c#
using System.Reactive.Concurrency;
using System.Windows;
using System.Windows.Threading;

namespace WpfApp1;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private MainWindow? ChildWindow;

    private void Button1_Click(object sender, RoutedEventArgs e)
    {
        if (ChildWindow is not null) return;
        NewThreadScheduler scheduler = NewThreadScheduler.Default;
        var context = SynchronizationContext.Current;
        var AnotherUIThread = new Thread(_ =>
        {
            ChildWindow = new MainWindow();
            ChildWindow.ShowDialog();
            Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.SystemIdle);
            Dispatcher.Run();
            ChildWindow = null;
        });
        AnotherUIThread.SetApartmentState(ApartmentState.STA);
        AnotherUIThread.IsBackground = true;
        AnotherUIThread.Start();
    }

    private void Button2_Click(object sender, RoutedEventArgs e)
    {
        if (ChildWindow is null) return;
        // ChildWindowを閉じる処理
    }
}
```
元記事では`Window.Closed`でしていた`Dispatcher`のshutdownを、`ShowDialog()`の後に変更しています。
また、`IsBackground`を設定しました。
起動はこれで良しとします。

# 3. 別スレッドのWindowを閉じる
ここからが、いばらの道の始まりです。

## 3.1. 無策で挑む
まず、想像に難しくありませんが単純な以下のコードでは例外が発生します。
```c#
    private void Button2_Click(object sender, RoutedEventArgs e)
    {
        if (ChildWindow is null) return;
        ChildWindow.Close(); // 例外発生
    }
```
`ChildWindow`は別スレッドで走っているので、そのスレッドで`Close()`しないといけません。

## 3.2. ObserveOnの登場
「はいはい、`Observable`からの`ObserveOn`ね。」と思ったあなた、お目が高い。その方針で進めます。
`ReactiveProperty`でコマンドを作り、
```c#
private ReactiveCommandSlim CloseCommand = new();
```
`Button2_Click()`で実行します。
```c#
private void Button2_Click(object sender, RoutedEventArgs e)
{
    if (ChildWindow is null) return;
    CloseCommand.Execute();
}
```
## 3.3. DispatcherScheduler.Current
あとは、別スレッドで`window.ShowDialog()`する前に、
```c#
CloseCommand.ObserveOn(DispatcherScheduler.Current).Subscribe(_ => window.Close());
```
とやるだけ・・・のはずが、`NET8`には`DispatcherScheduler`が無いのです。`NetFramework`時代はこの手法を使っていたのに、さて困りました。
## 3.4. Scheduler.CurrentThread
この辺りのAPIは`NET`移行時に整理されたようです。似たような名前の`Scheduler.CurrentThread`を使ってみます。
```c#
CloseCommand.ObserveOn(Scheduler.CurrentThread).Subscribe(_ => window.Close());
```
はい、これも失敗。ドキュメントを読んでみます。

https://learn.microsoft.com/ja-jp/previous-versions/dotnet/reactive-extensions/hh229285(v=vs.103)

>解説
>CurrentThread スケジューラは、元の呼び出しを行うスレッドで実行されるアクションをスケジュールします。 アクションはすぐには実行されませんが、キューに配置され、現在のアクションが完了した後にのみ実行されます。

つまり、ここでいう`CurrentThread`とは絶対的ではなく相対的であり、呼び出しスレッドで実行するという意味です。罠だよ、こんなの。

## 3.5. SynchronizationContext.Current
`ObserveOn`は`IScheduler`もしくは`SynchronizationContext`を引数にします。ここまでは前者で検討しましたが、ここでは後者で攻めてみます。
これまでと同様に、`ShowDialog()`の手前に1行加えます。
```c#
CloseCommand.ObserveOn(SynchronizationContext.Current).Subscribe(_ => window.Close());
```
ここでコンパイラから、「引数が`null`かもしれない」と警告が。念のため、
```c#
var context = SynchronizationContext.Current ?? throw new Exception();
CloseCommand.ObserveOn(context).Subscribe(_ => window.Close());
```
としたところ、なんと`context`が`null`でした。万策尽きた!!

## 3.6. SynchronizationContext.Current 解決編

https://crocus7724.hatenablog.jp/entry/2016/09/17/164429

上記事をよると、UIスレッド以外では`null`が返ってくるとのことです。
今回はUIスレッドで実行しているつもりですが、`window.ShowDialog()`の手前なので、まだUIスレッドと認識されていないのではないか? と想定しました。
`window.ShowDialog()`の後では、閉じた後でないと実行されないので無意味です。そこで、`Loaded`に記述しました。
```c#
ChildWindow.Loaded += (_, _) =>
{
    var context = SynchronizationContext.Current ?? throw new Exception();
    CloseCommand.ObserveOn(context).Subscribe(_ => ChildWindow.Close());
};
```
これが当たりで、無事閉じられるようになりました。

# 4. 結論
```c#
using Reactive.Bindings;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Threading;

namespace WpfApp1;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private MainWindow? ChildWindow;
    private ReactiveCommandSlim CloseCommand = new();

    private void Button1_Click(object sender, RoutedEventArgs e)
    {
        if (ChildWindow is not null) return;
        NewThreadScheduler scheduler = NewThreadScheduler.Default;
        var context = SynchronizationContext.Current;
        var AnotherUIThread = new Thread(_ =>
        {
            ChildWindow = new MainWindow();
            ChildWindow.Loaded += (_, _) =>
            {
                var context = SynchronizationContext.Current ?? throw new Exception();
                CloseCommand.ObserveOn(context).Subscribe(_ => ChildWindow.Close());
            };
            ChildWindow.ShowDialog();
            Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.SystemIdle);
            Dispatcher.Run();
            ChildWindow = null;
        });
        AnotherUIThread.SetApartmentState(ApartmentState.STA);
        AnotherUIThread.IsBackground = true;
        AnotherUIThread.Start();
    }

    private void Button2_Click(object sender, RoutedEventArgs e)
    {
        if (ChildWindow is null) return;
        CloseCommand.Execute();
    }
}
```

# 99. 親の記事に戻る
[Autodesk Inventor API Hacking (概略)](https://qiita.com/yamarah/items/cee08eb8f2b7aecbda83)