WPFでWinFormsやXAMLデザインに頼らず、WebView2を使いたい！

私です。お母様とこれを観ている方々、お元気ですか。
ふと「WebView2を試したいなぁ」と思い立って検索してみると、XAMLデザインによる方法はたらふく出てくるのですが、ソースレベルになると極端に情報が薄くなってきます。
偶に見つけても`WinForms`による方法だったりで残念です。

# 前準備

:::note info
本稿はhow toモノではありません。Microsoft Edge WebView2の基礎が少しある方への応用になります。
:::

- 対象フレームワークは ~~.NET5~~ .NET6([LTS](https://learn.microsoft.com/ja-jp/lifecycle/products/microsoft-net-and-net-core)) です。
- UIフレームワークは`WPF`をメインとします。`WinForms`でもUIのコードと関係ない箇所なら共通する部分は多いです。
  - `WPF`をメインとするなら[プロジェクトファイル](https://docs.microsoft.com/ja-jp/dotnet/desktop/winforms/migration/?view=netdesktop-5.0#project-file)の`UseWindowsForms`要素は`false`にするか削除するべきです。私はごちゃ混ぜにして泣かされました。混乱の元です。

なおソースの**ライセンスはパブリックドメイン**としますから、コピペして自由に使って下さい（名前空間をそのまま使うと私は両手を挙げて喜びを表します、参照エラーには気をつけて）。

# 専用のクラスを作って流れを観る

本稿ではUIについてほとんど触れず、やりたいことの大半はWebView2に関するソースを書くことに成ります。

主要機能を以下の専用クラスにまとめます。

- 専用クラスは`WebView2Controller`と命名します。
- メインのウインドウは`MainWindow`と命名します。
- UIのパッケージに`Microsoft.Web.WebView2.Wpf`を使います（私はココでも泣きました）。

また

- ウインドウタイトル制御は`MainWindow.xaml.cs`ファイル
- WebView2の制御は`WebView2Controller.cs`ファイル

このように仕事内容を分けておきます[^shigoto]。

とりあえず、`WebView2Controller`のコアなコードを書いていきます。

[^shigoto]: 個人的に`*.xaml.cs`ファイルに色々詰めるのが好きでは無い事と、まあまあのソース量になるので麺類にはしたくありません。

```c#:WebView2Controller.cs
using Microsoft.Web.WebView2.Core;
using System;
using System.Diagnostics;

namespace STSynthe.WebView2
{
    public class WebView2Controller
    {
        private readonly Microsoft.Web.WebView2.Wpf.WebView2 webView2 = new();

        public WebView2Controller()
        {
            this.webView2.CoreWebView2InitializationCompleted += this.CoreWebView2Initialization;
        }

        private void CoreWebView2Initialization(object sender, CoreWebView2InitializationCompletedEventArgs e)
        {
            if (e.IsSuccess)
            {
                Debug.WriteLine("初期化成功");
            }
            else
            {
                Debug.Fail("CoreWebView2の初期化に失敗しました。");
            }
            Debug.WriteLine("CoreWebView2InitializationCompleted");
        }

        public async void Navigate(string uri)
        {
            if (this.webView2.CoreWebView2 is null)
                await this.webView2.EnsureCoreWebView2Async();
            this.webView2.CoreWebView2.Navigate(uri);

            Debug.WriteLine("Navigate");
        }

        public Microsoft.Web.WebView2.Wpf.WebView2 GetWebView2()
        {
            return this.webView2;
        }
    }
}
```

必要最低限だとこのぐらい。コメント付けると長くなるので諸々の説明は後記にて。
この段階でプログラムの流れも観てみたいので`MainWindow.xaml`に`MainWindow`ウインドウを実装していく。

```xml:MainWindow.xaml
<Window x:Class="STSynthe.SampleWebView2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:STSynthe.SampleWebView2"
        mc:Ignorable="d"
        Title="ぶらうざ" Width="1280" Height="720"/>
```

今は最低限の実装に留めるため、`Window`以外の要素は挿入しない。
次に`MainWindow.xaml.cs`に`MainWindow`を制御するコードを実装する。

```c#:MainWindow.xaml.cs
using STSynthe.WebView2;
using System.Windows;
using System.Windows.Controls;

namespace STSynthe.SampleWebView2
{
    public partial class MainWindow : Window
    {
        private readonly DockPanel dockPanel = new();// DockPanelのインスタンス生成
        private readonly WebView2Controller webView2Controller = new();// WebView2Controllerのインスタンス生成

        public MainWindow()
        {
            // DockPanelの子要素にWebView2を追加する
            this.dockPanel.Children.Add(this.webView2Controller.GetWebView2());
            // Windowの子要素にDockPanelを追加する
            this.AddChild(this.dockPanel);

            // ナビゲートを指定
            this.webView2Controller.Navigate("https://www.google.com/");

            this.InitializeComponent();
        }
    }
}
```

横着してるが、DockPanelの子要素に既存のWebView2要素を追加する事で、ウインドウサイズを自動で合わせてくる。
わざわざイベント作ってリサイズさせるコードを省くことはできる。

これで最低限の実装は完了したので、デバッグを走らせると意図したデバッグメッセージがコンソールに出力されている。

![2022-09-27_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70146/13e751c8-a708-e534-66e9-89ebb1825885.png)

そしてブラウザ画面、Googleが表示されるウインドウが立ち上がってくる。

![2022-09-27_002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70146/d4c8611a-dee1-e147-6938-f7349f417d0e.png)

# 最初は動かないCoreWebView2を使えるようにする

[CoreWebView2](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.web.webview2.core.corewebview2)は名前の通り、WebView2のコアに相当するAPI。
しかしそのまま使うことはできない。

https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.web.webview2.wpf.webview2#remarks

備考によるとEdgeブラウザーの起動は重く、時間を要する。
なので`WebView2`インスタンス生成直後の`WebView2.CoreWebView2`は`null`を返すから、そのままでは使えない。

`WebView2.CoreWebView2`を介す処理は、**初期化完了まで待つ**。そうすることで`null`に成らない。待ってあげないと「`Microsoft.Web.WebView2.Wpf.WebView2.CoreWebView2.get が null を返しました`」と怒られるからだ。

初期化完了を待つには、ふたつの方法がある。

| 名前 | `CoreWebView2InitializationCompleted` | `EnsureCoreWebView2Async` |
|:----:|:----|:----|
| 種類 | イベント | 非同期メソッド |
| 実行順位[^1] | 前（登録順） | 後 |
| 個人的な使い分け | 一度だけの処理、イベント登録 | 繰り返して使う処理 |

[^1]: [リファレンス](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.web.webview2.wpf.webview2.ensurecorewebview2async#returns)自体に「実行順位」とは書かれてない（利便上そうのように記載しているだけ）。リターン章にて、その旨が記載されているのでそちら参照して下さい。また留意点として欲しい。

それぞれの使い方は次項にて。

## イベント`CoreWebView2InitializationCompleted`

一つの方法として[`CoreWebView2InitializationCompleted`](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.web.webview2.wpf.webview2.corewebview2initializationcompleted)イベントを使う。
名前の通り、CoreWebView2が初期化完了後に諸々を行うイベント。

試しに専用クラスの`WebView2Controller`に変更を加えて、どうるなかを確認する。

まずは初期化完了を待たずに`WebView2.CoreWebView2.Settings.AreDevToolsEnabled`を指定する。
`false`を渡すことで開発者ツールを無効化[^DevTool]するが、もちろん例外が発生して動くことは無い。

```diff_c_sharp:WebView2Controller.cs
// WebView2.CoreWebView2がnullでBADな例
         public WebView2Controller()
         {
+            this.webView2.CoreWebView2.Settings.AreDevToolsEnabled = false;
             this.webView2.CoreWebView2InitializationCompleted += this.CoreWebView2Initialization;
         }
```

次にイベント`CoreWebView2InitializationCompleted`を使う。イベントとして登録したメソッドにさっきの追記コードを移植する。

```diff_c_sharp:WebView2Controller.cs
         private void CoreWebView2Initialization(object sender, CoreWebView2InitializationCompletedEventArgs e)
         {
             if (e.IsSuccess)
             {
                 Debug.WriteLine("初期化成功");
+                this.webView2.CoreWebView2.Settings.AreDevToolsEnabled = false;
             }
             else
             {
                 Debug.Fail("CoreWebView2の初期化に失敗しました。");
             }
             Debug.WriteLine("CoreWebView2InitializationCompleted");
         }
```

![2022-09-27_003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70146/86624e42-56ee-dc66-d684-b7fa41eb099b.png)

今度はコンテキストメニューから開発者ツールが消えて成功[^DevTool]。

[^DevTool]: コンテキストメニュー及びキーボードショートカットからも開発者ツールの起動が無効化される。

## 非同期メソッド`EnsureCoreWebView2Async`

まず先に専用クラス`WebView2Controller`の`Navigate`メソッドの事について。

`WebView2Controller`の`Navigate`メソッドは`WebView2.CoreWebView2.Navigate`のラッパーで、ブラウザにURLを渡してそのアドレスに遷移してもらうものです。
もう先ほどのでお気づきかと思いますが、これも`CoreWebView2`を介すので初期化完了を待たなければ成りません。

そこでもう一つの方法として`WebView2.EnsureCoreWebView2Async`を使っています。

https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.web.webview2.wpf.webview2.ensurecorewebview2async

`CoreWebView2`の初期化が完了すると次のコードを実行します。
非同期なのでAsync/Awaitが使われています。

`WebView2Controller`の`Navigate`はいつどんなときに呼び出されても安全に動作するよう、このような方法が採られてます。

# 適当な応用集

応用自体は探せばまあまああるので適当。

:::note warn
警告
`WebView2Controller`を改造して使うので、上記を読み飛ばすと**分からなくなります**。
:::

## 設定で色々無効化する

xamlのプロパティでは変更できない部分。

```c#:WebView2Controller.cs
        private void CoreWebView2Initialization(object sender, CoreWebView2InitializationCompletedEventArgs e)
        {
            if (e.IsSuccess)
            {
                // 開発者ツールの無効化
                this.webView2.CoreWebView2.Settings.AreDevToolsEnabled = false;
                // コンテキストメニュー（右クリックメニュー）の無効化
                this.webView2.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false;
                // ダイアログ無効化
                this.webView2.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = false;
                // ステータスバー非表示
                this.webView2.CoreWebView2.Settings.IsStatusBarEnabled = false;
                // 内蔵されたエラーページを無効化
                this.webView2.CoreWebView2.Settings.IsBuiltInErrorPageEnabled = false;
                // ズームコントロールの無効化
                this.webView2.CoreWebView2.Settings.IsZoomControlEnabled = false;
            }
            else
            {
                Debug.Fail("CoreWebView2の初期化に失敗しました。");
            }
            Debug.WriteLine("CoreWebView2InitializationCompleted");
        }
```

留意点として、コンテキストメニューの無効化をするとキーによるページの「戻る」「進む」も無効化されます。

バージョンアップにつれて設定できる項目も増えているので、参考リンク先は偶には確認しておくことをすすめる。

### 参考

- [CoreWebView2.Settings Property](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.web.webview2.core.corewebview2.settings)

## 新しいウインドウを抑止する

JavaScriptによる`window.open`や、HTMLの属性`target="_blank"`、ユーザー操作による新しいウインドウを開かせないようにする。

```c#:WebView2Controller.cs
        private void CoreWebView2Initialization(object sender, CoreWebView2InitializationCompletedEventArgs e)
        {
            if (e.IsSuccess)
            {
                this.webView2.CoreWebView2.NewWindowRequested += this.NewWindowDeter;
            }
            else
            {
                Debug.Fail("CoreWebView2の初期化に失敗しました。");
            }
            Debug.WriteLine("CoreWebView2InitializationCompleted");
        }

        private void NewWindowDeter(object sender, CoreWebView2NewWindowRequestedEventArgs e)
        {
            // イベントを処理済みとして扱うなら新しいウィンドウは開きません。デフォルトはfalseです。
            e.Handled = true;
        }
```

これでどんな手法を採っても新しいウインドウは開かないように成る（`Handled`は「キャンセル」や「停止」という意味では無い）。
というかソースの注釈やリファレンスになんか書いてあったね。

> If set to true and no Microsoft.Web.WebView2.Core.CoreWebView2NewWindowRequestedEventArgs.NewWindow is set for window.open(), the opened WindowProxy is for a dummy window object and no window loads. The default value is false.

ちなみに[こちらの記事](https://qiita.com/NagaJun/items/4925a63ce7b93b80639e)では、ダミーの為にWebView2を生成して、それを`e.NewWindow`に代入してstopで停止させる手法です。
しかしダミーインスタンスは残り続ける為、リソースの消費も大きく無駄です。

新しいウインドウは抑止したいが、ページは遷移させたいのであれば

```c#:WebView2Controller.cs
        private void NewWindowDeter(object sender, CoreWebView2NewWindowRequestedEventArgs e)
        {
            e.NewWindow ??= this.webView2.CoreWebView2;
        }
```

`e.NewWindow`に既存の`CoreWebView2`を代入する処理に置き換える。あるいは

```c#:WebView2Controller.cs
        private void NewWindowDeter(object sender, CoreWebView2NewWindowRequestedEventArgs e)
        {
            e.Handled = true;// Handledプロパティにtrueを渡してイベントを処理済みとして扱う
            this.Navigate(e.Uri);// WebView2Controller内のNavigateを使ってe.Uriプロパティからページ遷移する予定のURLを渡す
        }
```

こっちのほうが感覚的に分かりやすいかも。

### 参考

- [CoreWebView2.NewWindowRequested Event](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.web.webview2.core.corewebview2.newwindowrequested)
- [CoreWebView2NewWindowRequestedEventArgs Class](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.web.webview2.core.corewebview2newwindowrequestedeventargs)

## `AllowDrop = false` not working?

本来WebView2の`AllowDrop`はデフォルトで`false`なのですが、明確的に指定してもファイルドロップされると反応してローカルファイルや他のURLに遷移してしまいます。`Drop`イベントも無反応で、キャンセルできず。

代替案として、「[#新しいウインドウを抑止する](#新しいウインドウを抑止する)」を参考にしてください。
動作的に新しいウィンドウを生成しようとしてくるので、その辺りで対応できます。

## ページのタイトルをウインドウのタイトルに表示させたい

CoreWebView2の制約もあるので、少し回りくどいが`CoreWebView2.DocumentTitleChanged`のラッパーを作る。

`WebView2Controller`側：

```diff_c_sharp:WebView2Controller.cs
         private readonly Microsoft.Web.WebView2.Wpf.WebView2 webView2 = new();
+        public event EventHandler<string> DocumentTitleChanged;

// ～～中略～～

         private void CoreWebView2Initialization(object sender, CoreWebView2InitializationCompletedEventArgs e)
         {
             if (e.IsSuccess)
             {
+                this.webView2.CoreWebView2.DocumentTitleChanged += this.DocumentTitleChangedEvent;
             }
             else
             {
                 Debug.Fail("CoreWebView2の初期化に失敗しました。");
             }
             Debug.WriteLine("CoreWebView2InitializationCompleted");
         }

+        private void DocumentTitleChangedEvent(object sender, object e)
+        {
+            this.DocumentTitleChanged?.Invoke(this, this.webView2.CoreWebView2.DocumentTitle);
+        }
```

簡易的なイベント`DocumentTitleChanged`を作る。これを`MainWindow`側で登録・処理する。
面倒なので、独自の引数にせず、ドキュメントタイトルの`string`をダイレクトに渡す（名前で分かるよね）。

`MainWindow`側：

```diff_c_sharp:MainWindow.xaml.cs
// ～～中略～～
             this.AddChild(this.dockPanel);

+            this.webView2Controller.DocumentTitleChanged += (object sender, string e) => this.Title = $"{e} - ぷらうざ";
```

ページのタイトルが書き換わる度にタイトルも変わっているはず。オーバーフローを避ける為に文字数制限付けた方が良いので、各自工夫して欲しい。

一行で書く為に、無名関数をラムダ式で書いてるが、色々問題あるので、よいこのみんなは、名前のある関数で書くと良いです。

### 参考

- [CoreWebView2.DocumentTitleChanged Event](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.web.webview2.core.corewebview2.documenttitlechanged)

## ユーザーデータフォルダの指定

要はキャッシュなどの保存先。

https://docs.microsoft.com/ja-jp/microsoft-edge/webview2/concepts/user-data-folder

一応公式ページのコンセプトに「ユーザーデータフォルダ」の事について書かれてるが、無理矢理翻訳のせいか私の解読力が無いのかよく分からん。WebView2リファレンスの方が詳しいかもしれない。
詳しいリファレンスによるとユーザーデータフォルダの指定方法は二つ。
個人的にコードがまとまる方を選びたいので本項では`CoreWebView2CreationProperties`クラスを使う事とする。他はWebView2リファレンスを参照して欲しい。

```diff_c_sharp:WebView2Controller.cs
         private readonly Microsoft.Web.WebView2.Wpf.WebView2 webView2 = new();
+        private readonly Microsoft.Web.WebView2.Wpf.CoreWebView2CreationProperties coreWebView2CreationProperties = new();

// ～～中略～～

         public WebView2Controller()
         {
+            this.webView2.Initialized += this.WebView2Initialized;
             this.webView2.CoreWebView2InitializationCompleted += this.CoreWebView2Initialization;
         }

// ～～中略～～

+        private void WebView2Initialized(object sender, EventArgs e)
+        {
+            string userDataFolder = Environment.GetEnvironmentVariable("TEMP");
+            userDataFolder += @"\wv2";
+            if (Directory.Exists(userDataFolder))
+            {
+                try
+                {
+                    Directory.CreateDirectory(userDataFolder);
+                }
+                catch (Exception)
+                {
+                    this.webView2.Dispose();
+                    throw;
+                }
+            }
+            // ユーザーデータフォルダを指定する
+            this.coreWebView2CreationProperties.UserDataFolder = userDataFolder;
+            // CoreWebView2CreationPropertiesをセットする
+            this.webView2.CreationProperties = this.coreWebView2CreationProperties;
+        }
```

`CoreWebView2CreationProperties`インスタンスを生成し、それの`UserDataFolder`プロパティにユーザーデータフォルダのパスを指定する。
あとは`WebView2.CreationProperties`に`CoreWebView2CreationProperties`をセットする。

例の場合だと、`\Users\<ユーザー名>\AppData\Local\Temp`の中に`wv2`フォルダが作成され、CoreWebView2の初期化後、その中に色々ぶち込まれる。

注意点として

- `CreationProperties`への追加は、CoreWebView2の初期化が始まる前（インスタンス生成直後など）なら挿入箇所はどの行でも良いです。
 - 私が挿入位置を明確的にしたいが為に、関連の無いイベント`Initialized`を使っているだけです（FrameworkElementが初期化されたタイミングで発生します）。
- `CreateDirectory`しなくてもWebView2側がフォルダを作成しますが、パスチェックなどをこちらでやりたいのでこのようになってます。

## 選択されたテキストの抽出

マウスカーソルで選択したテキストを抽出する。
ドキュメントに介入するAPIはほとんど無く、基本JavaScriptを駆使することに成る。

```c#:WebView2Controller.cs
         private readonly Microsoft.Web.WebView2.Wpf.WebView2 webView2 = new();
         private string SelectedTextScriptId;

// ～～中略～～

         private void CoreWebView2Initialization(object sender, CoreWebView2InitializationCompletedEventArgs e)
         {
             if (e.IsSuccess)
             {
                 this.webView2.CoreWebView2.WebMessageReceived += this.WebMessageProcessor;
                 this.SelectedTextInitialize();
             }
             else
             {
                 Debug.Fail("CoreWebView2の初期化に失敗しました。");
             }
             Debug.WriteLine("CoreWebView2InitializationCompleted");
         }

         private void WebMessageProcessor(object sender, CoreWebView2WebMessageReceivedEventArgs e)
         {
             Debug.WriteLine(e.WebMessageAsJson);
         }
 
         private async void SelectedTextInitialize()
         {
             if (this.SelectedTextScriptId is null)
             {
                this.SelectedTextScriptId = await this.webView2.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(@"
new class {
	constructor() {
		window.document.addEventListener('mouseup', this.selectEvent);
	}

	selectEvent(event) {
		if (event.isTrusted && event.button === 0) {
			const selection = window.getSelection();
			if (!selection.isCollapsed && selection.type.includes('Range')) {
				console.log(`SelectedText: '${selection.toString()}'`);
				window.chrome.webview.postMessage({
					Type: 'SelectedText',
					Text: selection.toString()
				});
			}
		}
	}
}");
             }
         }
```

`CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync`を使ってテキストを抽出するJavaScriptを登録する。一度登録しておけばページ遷移しても消えることは無い。返り値のIDは例え使わなくても保存をする[^optimisation]。

[^optimisation]: ビルドの際に最適化され、使われてない変数はどのみち削除される。

`CoreWebView2.WebMessageReceived`イベントを使ってパッシブに受け取る。受け取ったメッセージが勝手にJSON化されるのでデシリアライズなりしておけば良い。

試しに何かの文字列を選択するとデバッグメッセージに「`{"Type":"SelectedText","Text":"【選択した文字列】"}`」と表示される。

以下は登録されたJavaScriptの部分を少し解説する。

```javascript
new class {
	constructor() {
		window.document.addEventListener('mouseup', this.selectEvent);
	}

	selectEvent(event) {
		if (event.isTrusted && event.button === 0) {
			const selection = window.getSelection();
			if (!selection.isCollapsed && selection.type.includes('Range')) {
				console.log(`SelectedText: '${selection.toString()}'`);
				window.chrome.webview.postMessage({
					Type: 'SelectedText',
					Text: selection.toString()
				});
			}
		}
	}
}
```

無名のclassが即座に実行されると、コンストラクタで`mouseup`イベントを登録する。
もし、ユーザー操作によるマウスの左ボタンを上げたときにテキストが選択されてたなら選択範囲の文字列を`window.chrome.webview.postMessage()`で送信します。

ここでなぜ、[`selectionchange`](https://developer.mozilla.org/ja/docs/Web/API/Document/selectionchange_event)イベントが使われてないかと思う方もいるでしょう。
それは選択される度にメッセージを送信しようとするので`mouseup`イベントで工夫している。というよりそれ以外の最適解が見つからなかったのだ。

### 参考

- [CoreWebView2.WebMessageReceived Event](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.web.webview2.core.corewebview2.webmessagereceived)
- [CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(String) Method](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.web.webview2.core.corewebview2.addscripttoexecuteondocumentcreatedasync)
- 【JavaScript】[EventTarget.addEventListener()](https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener)
- 【JavaScript】[window.getSelection](https://developer.mozilla.org/ja/docs/Web/API/Window/getSelection)
- 【JavaScript】[Element: mouseup event](https://developer.mozilla.org/en-US/docs/Web/API/Element/mouseup_event)

## 禁断？関連づけられたファイルをJavaScriptから呼び出す

むかし、HTA（HTML Application）という~~💩のような~~アプリがありました。
`new ActiveXObject("WScript.Shell")`にエクスキュート的なことをすることでいろんなアプリを立ち上げられたりできました。
今となってはセキュリティー面でよろしくはないが、仕事ツールの自動化としてはまだ使いどころはあるでしょう。普通じゃできない事をやれるのが.NET + WebView2のよいところ。

:::note alert
真っ先に**セキュリティー**面を考慮し、安全を最優先にしましょう。
HTAのような考え方がありますが、**最新のWindowsでHTAやActiveXが無効化されている理由をよく考えてください**。

コードは簡単に実行できないよう端折っていますので各自工夫して補完する。
この件に関して一切の問い合わせされても対応しません。自己責任で。
:::

### 専用クラスを作って`CoreWebView2.AddHostObjectToScript`に投げる

制約はあるが、C#側で作ったクラスをページ内のJavaScriptで実行することができる。そのために専用クラスを作る。

```c#:???.cs
    // 実行を許可する拡張子
    private static readonly string[] s_allowExtensions = new string[7] { "txt", "htm", "html", "doc", "docx", "xls", "xlsx" };

    private static Process s_currentProc;

    public void FileExecute(string? filename)
    {
        // nullと空白を許さない
        if (String.IsNullOrEmpty(filename))
        {
            return;
        }

        // フルパスではない or 拡張子なしを許さない or ファイルが存在しないことを許さない
        if (Path.IsPathFullyQualified(filename) == false || Path.HasExtension(filename) == false || File.Exists(filename) == false)
        {
            return;
        }

        string ext = Path.GetExtension(filename);

        // filenameが許可される拡張子を持ち合わせているか？
        foreach (string allow in s_allowExtensions)
        {
            if (ext.EndsWith("." + allow, StringComparison.OrdinalIgnoreCase))
            {
                try
                {
                    if (s_currentProc != null && s_currentProc.HasExited == false)
                    {
                        break;// 連続実行の抑止により、前回のプロセスが起動中なら実行しない
                    }

                    s_currentProc = Process.Start("cmd.exe", "/K \"" + filename + "\"");// 実行

                }
                catch (Exception e)
                {
                    Debug.WriteLine($"Crash!! ({e.Message})");
                }
                break;
            }
        }
    }
```

作り終えたら`CoreWebView2.AddHostObjectToScript("funcs", new ???())`に投げる。第一関数の`funcs`はJavaScript側で関数群を受け取るオブジェクト名になる。インスタンスの生成は必要ないが、全ての関数群はAsync/Awaitになる事に留意。第二関数は先ほど作った専用クラスのインスタンス生成してやるだけ。

### ページ内のJavaScriptから実際に呼び出す

ローカルに適当なhtmlファイルを用意する。
JavaScriptからWebView2のホストオブジェクトを呼び出し、続けて上記で`CoreWebView2.AddHostObjectToScript`に登録したオブジェクト名、専用クラスから呼び出すメソッドを指定する。完全修飾名で書くと`chrome.webview.hostObjects.funcs.FileExecute`になる。

`FileExecute`の引数は実行を許されている拡張子かつフルパスでなければならない（`FileExecute("D:/適当なパス/内緒.txt")`このように）。
うまくいけば関連づけられたテキストエディタが相当ファイルを開いて起動してくれる。失敗してもスンとも反応しないので色々苦労することになるだろう。

### このまま実装してはならない

実行できる拡張子を制限したり、フルパスでなければ起動しなかったり、連続実行を抑止するために…　と、コードを見てもらえればいろいろな対策をとっていますがこれでも甘いでしょう。

なのでこのまま使ってはならない。

### 参考

- [CoreWebView2.AddHostObjectToScript(String, Object) Method](https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.web.webview2.core.corewebview2.addhostobjecttoscript)

# 最後

QiitaでWebView2に関する記事は少なく、なんか回りくどい感じがあったので自分で調べることにしました。

というより公式に載ってない情報がちらほらあるような…。

（2021年8月8日追記）

今までおまけ程度だった「[適当な応用集](#%E9%81%A9%E5%BD%93%E3%81%AA%E5%BF%9C%E7%94%A8%E9%9B%86)」を気が向いたら追記していくスタイルでしたが区切りを付けたいのでいったん終了とします。読んで頂きありがとうございました。

（2022年9月28日追記）

1年ぶりに誤字とスクリプトの誤り修正、画像追加と、謎の項目も追加しました。