C#でMonacoEditor

WebView2でMonaco Editorを使用する。
Monaco Editorを至る所で目にするので（ブラウザ・デスクトップアプリ）
C#のWPFアプリにも取り入れてみました。
electronでやるのが一般的

- Monaco Editor

https://microsoft.github.io/monaco-editor/

## 環境
- C# 8か9くらい
- .NET6
- WPF

## ソースコード
- 実装

https://github.com/Yuki4-dev/SimpleMonaco

## 画面
WPFで動作するシンプルなMonacoEditorです。
コマンドライン引数を指定することで、開くファイルと言語シンタックスを選択することができます。 
Ctrl＋Sで名前を付けて保存or上書き保存を行うことができます。
（以下の画面は言語シンタックスをJavaScriptに指定）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688992/b46d5793-9fd1-fc2b-2c3f-feaacf7a69bd.png)

## Monaco Editor
npmでとりあえず、ダウンロード
```
npm install monaco-editor@{VERSION}
```

## XAML
NugetでWebView2を追加
WebView2を表示するだけの画面
```MainWindow.xaml
<Window
    x:Class="SimpleMonaco.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:local="clr-namespace:SimpleMonaco"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:wpf="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
    Title="SimpleMonaco"
    Width="450"
    Height="650"
    BorderThickness="2"
    FontFamily="Cascadia Mono"
    mc:Ignorable="d">

    <Grid>

        <wpf:WebView2
            x:Name="MonacoWebView"
            Margin="-1"
            HorizontalAlignment="Stretch"
            VerticalAlignment="Stretch"
            NavigationStarting="MonacoWebView_NavigationStarting" />

        <TextBlock
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="48"
            Text="Simple Monaco" />

    </Grid>

</Window>
```

## C#ソースコード
- WebView2へ、表示したいhtmlのパスを設定する。
Directory.GetCurrentDirectory()だと、コンテキストメニューのプログラムで開くから起動したとき
うまくアプリのパスを取得できないので、Assemblyクラスを使用。
```MainWindow.cs
var htmlUri = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + @"\html\index.html";
MonacoWebView.Source = new Uri(htmlUri);
```

- JavaScript側とC#側の疎通をとるためのオブジェクト 
```MainWindow.cs
[ComVisible(true)]
public class MonacoModel
{
    public event Action<MonacoModel>? TextChanged;

    public event Action<MonacoModel>? RequestSave;

    private string _Text = string.Empty;
    public string Text
    {
        get => _Text;
        set
        {
            if (_Text != value)
            {
                _Text = value;
                TextChanged?.Invoke(this);
            }
        }
    }

    public string Language { get; set; } = string.Empty;

    public void OnRequestSave()
    {
        RequestSave?.Invoke(this);
    }
}
```
- JavaScript側でC#のクラスを操作できるようにする。
WebViewのNavigationStartingイベントで、オブジェクトをWebViewに渡す。
CoreWebView2が生成されるのタイミングがおそらく、WebViewのLoadedイベント以降なので
このタイミングで実施する。
また、NavigationCompletedイベントだと、htmlロード時に実行されるJavaScriptからは参照できないので注意。
（最初はNavigationCompletedで実施していたが、うまくJavaScript側で参照できず困った。。。）

```MainWindow.cs
 private void MonacoWebView_NavigationStarting(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationStartingEventArgs e)
 {
     if (MonacoWebView.CoreWebView2 != null)
     {
         MonacoWebView.CoreWebView2.AddHostObjectToScript("model", _Model);
     }
 }
```
## Monaco Editorを表示するhtml
ウィンドウ全面にエディターを表示したいので、適切なStyleを設定する。
Monaco Editor自体にスクロールバーが表示されるので、ブラウザのスクロールバーは非表示にする。
C#のオブジェクトは「chrome.webview.hostObjects.」配下にある。
```index.html
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
    <style>
        html, body {
            height: 100%;
            margin: 0;
        }
        ::-webkit-scrollbar {
            display: none;
        }
        #container {
            height: 100%;
            width: 100%;
        }
    </style>
</head>
<body>
    <div id="container"></div>
    <script src="node_modules/monaco-editor/min/vs/loader.js"></script>
    <script>
        require.config({ paths: { 'vs': 'node_modules/monaco-editor/min/vs' } });
        require(['vs/editor/editor.main'], async function () {
            const editor = monaco.editor.create(document.getElementById('container'), {
                theme: "vs-dark",
                fontFamily:'Cascadia Code',
                // C#のオブジェクトのプロパティや関数の戻り値を取得する際はawait
                // 使用する言語と初期表示時の値を設定
                language: await chrome.webview.hostObjects.model.Language,
                value: await chrome.webview.hostObjects.model.Text,
            });
            const model = editor.getModel();
            // エディターのテキスト変更イベント
            model.onDidChangeContent((event) => {
                // C#側のオブジェクトのプロパティを更新
                chrome.webview.hostObjects.model.Text = model.getValue();
            });
            editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
                // Ctrl + Sが押されたとき、C#側の保存イベントをコールする。
                chrome.webview.hostObjects.model.OnRequestSave();
            })
            window.addEventListener("resize", () => {
                editor.layout();
            });
        });
    </script>
</body>
</html>
```
## VisualStudio 
exe出力先にhtmlファルダを配置するためにビルドイベント追加
```
xcopy "$(ProjectDir)\html" "$(OutDir)\html" /i /s /y
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688992/b3084e9d-aa4f-9e70-e41a-89cf98f7f423.png)

## 配布
他のPCでWebView2を動作するためには、動作させるPCにWebView2 ランタイムをインストールさせる必要がある。
- インストールページ

https://developer.microsoft.com/en-US/microsoft-edge/webview2/

- 参考

https://docs.microsoft.com/ja-jp/microsoft-edge/webview2/concepts/distribution

## まとめ
簡単に取り込めて、行番号、シンタックスハイライトや検索などの機能も充実しているため便利です。
C#系のTextBoxはかなり重いため、大量の文字を処理するの場合はMonacoEditorのほうがいいかもしれませんね。
ただ、MSIXでパッケージ化したらhtml読み込み時、JavaScriptのrequireが呼び出せ事象が発生したので
マニフェストファイルに何らかの宣言が必要？そうです。

MicrosoftのDevToyのソースコードをが参考になりました。

https://github.com/veler/DevToys