C# WebView2を通じてWebサーバーなしでJavaScriptからローカルファイルを読み書き

TL;DR

Webサーバーを使用せずに、file:///でブラウザーで開かれたhtmlファイルは、JavaScriptからPCのファイルシステムにアクセスできない。
WebView2コントロールにhtmlファイルを読み込み、JavaScriptがメッセージでWebView2に指示すれば、.NetのSystem.IOを利用してPCファイルの読み書きが可能になる。

目次

完成イメージ

アプリのメイン画面


このアプリ例は以下の3つの部分に構成されている:

  1. C#で書いたWinFormsアプリ
    → Formに含まれるControlはWebView2のみ
  2. Page.html
    → 上記WebView2のソースに指定
  3. Page.js
    → 上記htmlファイルのscriptタグのsrcに指定

わざわざC#とJavaScriptを組ませた理由

確かにC#のアプリを作ればファイルシステムへのアクセスはし放題で、わざわざJavaScriptと組んでやる必要がないと思います。
自分も個人用のアプリはほとんどC#だけで作れたのですが、この前ひとつ不便を感じたので、この方法にたどり着きました。それは、HTMLコードを生成させる機能を実装するときでした。
C#で絶対できないわけではないのですが、やはりJavaScriptからHTMLコードを生成したほうが楽だと感じました。
ただ、自分はWebサーバーを立ち上げるのが面倒なのでしたくないんです。その場合、セキュリティ上の制限で、JavaScriptからPCのファイルを読み書きできません。
ということで、JavaScriptとC#を組み合わせることにしました。
C#アプリがブラウザーの代わりになって、JavaScriptのできないことをC#にやらせるような感じです。

WebView2とは

全称は"The Microsoft Edge WebView2 control"だそうです。
詳しくはいつでもGoogle先生に聞けますので、平たく自分の理解を書きますと、

  • 先進的なMicrosoft Edge (Chromium)をベースとしたコントロール
  • Win32/WinForms/WPF/WINUI3多彩なプラットフォームをサポート
  • ページとWebView2間でMessageのやり取りができるので、JS側でできないことを.Netにさせられる、vice versa。
    などなど。

Step 1. プロジェクト作成

  1. Visual Studio 2019側:
    Windows Forms App/.Net 5.0プロジェクト
  2. Visual Studio Code側:
    htmlとjs或いはtsファイルを含むフォルダー

Step 2. page.htmlを作成

page.htmltextareaひとつと、"input"ボタン3つです。

<!DOCTYPE html> <html> <head> <meta http-equiv="content-type" charset="UTF-8"> </head> <body> <textarea id="txtEdit" style="width: 400px;height: 300px;"></textarea> <div> <input id="btnSave" type="button" value="Save"> <input id="btnOpen" type="button" value="Open"> <input id="btnClear" type="button" value="Clear"> </div> <script src="./page.js"></script> </body> </html>

step 3. page.jsを作成

page.ts側、TypeScriptで書いたので、tscでjsを出力する必要があるが、ここでは省略。

var txtEdit=document.querySelector("#txtEdit") as HTMLTextAreaElement; var btnSave=document.querySelector("#btnSave") as HTMLInputElement; var btnOpen=document.querySelector("#btnOpen") as HTMLInputElement; var btnClear=document.querySelector("#btnClear") as HTMLInputElement; btnSave.addEventListener("click",()=>{ let msg:Message={type:"save",msg:txtEdit.value}; //Saveボタンが押されると、saveする旨とセーブの内容をWebView2へ送信する。 window.chrome.webview.postMessage(msg); }); btnOpen.addEventListener("click",()=>{ let msg:Message={type:"open",msg:""}; //Openボタンが押されると、Openする旨をWebView2へ送信する。 window.chrome.webview.postMessage(msg); }); //WebView2からのメッセージを受信して、textareaに内容を反映 window.chrome.webview.addEventListener("message", txt => { txtEdit.value=txt.data; }); btnClear.addEventListener("click",()=>{ txtEdit.value=""; }); interface Message{ type:string, msg:string }

step 4. C#アプリ側を実装

NuGetでWebView2をインストール

VS2019のメニューバーから[Tools]→[NuGet Package Manager]→[Browse]でWebView2を検索してインストール

NugetでWebView2をインストール


FormにWebView2コントロールを追加

DockのプロパティをFillにする。

MessageのクラスをJavaScript側と一致するように定義

public class Message {//メッセージの形式を定義する public string type { get; set; } public string msg { get; set; } }

MessageのStringをMessageクラス型に変換する関数を定義

using System.Text.Json; using System.Text.Encodings.Web; public Message JsonStringToMessage(string input) {//Json stringをMessage型に変換 JsonSerializerOptions options = new JsonSerializerOptions(); options.Encoder = JavaScriptEncoder.Create(System.Text.Unicode.UnicodeRanges.All); return JsonSerializer.Deserialize<Message>(input, options); }

FileをSaveとOpenする関数を定義

private async static Task SaveFile(string path, string content) {//ファイルをセーブする await Task.Run(() => { File.WriteAllText(path, content); }); } private async static Task<string> LoadFile(string path) {//保存されたファイルをロードする if (!File.Exists(path)) { File.Create(path); return string.Empty; } string content = await Task.Run(() => File.ReadAllText(path)); return content; }

以上で準備が出来たので、ここからはWebView2の操作に入る

WebViewを初期化する関数を作成

async void InitWebViewAsync() {//WebView2を初期化する await wv2.EnsureCoreWebView2Async(null); }

最後はWebView2を初期化し、メッセージに対する挙動を指定

public Form1() { InitializeComponent(); //WebView2を初期化してソースをローカルhtmlファイルに指定する const string HTML_PATH = "E:\\test\\Wv2Js\\js\\page.html"; InitWebViewAsync(); wv2.Source = new Uri($"file:///{HTML_PATH}"); wv2.WebMessageReceived +=async (s, e) => {//WebView2はWebMessageを受信したら以下のように反応する Message msg = JsonStringToMessage(e.WebMessageAsJson); //受信したメッセージはstringなので、Message型に変換する switch (msg.type) { case "save": await SaveFile("file.txt", msg.msg); break; case "open": string txtLoaded = await LoadFile("file.txt"); //ファイル内容を読み取ってWebMessageでJavaScript側に送る wv2.CoreWebView2.PostWebMessageAsString(txtLoaded); break; default: break; } };

参考:Debug方法

C#側のDebug方法

通常通りにVisual StudioのDebug機能を利用します。

JavaScript側のDebug方法

実行されているアプリの画面で、キーボードのF12を押して、WebView2のDevToolsを呼び出します。
そしてSourceタブで上のステップで作成されたJavaScriptファイルを開いて、ブレークポイントを設置したりDebugできます。

後書き

WebView2を使えば、JavaScriptから.Netライブラリの莫大な機能が使えて、新たなドアが開いた感じがします。
今後アプリを作成するときに、ひとつの選択肢として置いておきたいと思います。

コメント


  1. 失礼します。
    本記事についての質問等は可能でしょうか?
    よろしくお願いいたします。

    返信削除
    返信
    1. ご返信遅くなり失礼しました。ご質問どうぞ。

      削除
    2. はじめまして!
      c#初心者です。
      早速ですが、本記事の通りにコード作成・デバッグ中ですが、以下の例外が発生し対処策を調べています。

      {例外}
      System.Text.Json.JsonException:
      'The JSON value could not be converted to System.Collections.Generic.List`1[WinFormsApp.Form1+Message]. Path: $ | LineNumber: 0 | BytePositionInLine: 56.'

      動作環境など
      ※1 page.html、 page.js(page.ts より生成)、 c# WinFormsアプリのコードは、記事と同じです。
      ※2 デバッグ環境も記事と同じです。

      解決策をお教えいただければと存じます。
      よろしくお願いいたします。

      削除
    3. こんばんは、ご質問を拝見しました。
      まず、例外が発生した関数はpublic Message JsonStringToMessage(string input)のはずです。
      この関数は以下のようにコールされます:
      Message msg = JsonStringToMessage(e.WebMessageAsJson);

      この関数の用途はjs側から送られてきたjsonの文字列をC#で定義したMessage型に変換するためです。
      つまり、inputはこんな感じになっていないと、Message型に変換できませんよね:
      input = "{\"type\":\"save\",\"msg\":\"test\"}"

      でも、GoodStaffさんのコメントではinputはtypeとmsgの部分がないようです:
      input = "\"file:///C://Users//Hoge//NodejsWebApp1222//page.html\""

      inputつまりe.WebMessageAsJsonは、js側から送られてくるメッセージになります。
      let msg:Message={type:"save",msg:txtEdit.value};
      のような感じでjsで定義されます。

      なぜかjs側から正しいフォーマットで送れていないようですね。
      記事通りでしたらいけるはずですが、js側で改良等は行われていないでしょうか?

      削除
    4. さっそくの返信、ありがとうございます。
      ※1 js側を調べてDebugしてみます。
      ※2 興味深い記事と情報の提供、感謝しております & また何かありましたらこりずにお願いいたします。

      削除
  2. 追記:

    例外発生個所を載せておきます。

    return JsonSerializer.Deserialize(input, options);

    プログラム実行時に、上記コードで例外が発生します!!

    どうぞよろしくお願いいたします。

    返信削除
  3. 追記2:

    inputの値は、下記のようにpage.htmlへのパスを入れています。

    input = "\"file:///C://Users//Hoge//NodejsWebApp1222//page.html\""

    以上です。

    返信削除
  4. 追記3:

    念のため、呼び出し側コードも載せておきます。

    //WebView2を初期化してソースをローカルhtmlファイルに指定
    const string HTML_PATH = @"C:\\Users\\Hoge\\NodejsWebApp1222\\page.html";
    InitWebViewAsync_webView25();
    webView25.Source = new Uri($"file:///{HTML_PATH}");

    webView25.WebMessageReceived += async (s, e) =>
    {//WebView2はWebMessageを受信したら以下のように反応する
    Message msg = JsonStringToMessage(e.WebMessageAsJson);
    //受信したメッセージはstringなので、Message型に変換
    switch (msg.type)
    {
    case "save":
    await SaveFile("file.txt", msg.msg);
    break;
    case "open":
    string txtLoaded = await LoadFile("file.txt");
    //ファイル内容を読み取ってWebMessageでJavaScript側に送る
    webView25.CoreWebView2.PostWebMessageAsString(txtLoaded);
    break;
    default:
    break;
    }
    };

    返信削除

コメントを投稿

個人情報を記入しないようご注意ください

このブログの人気の投稿

C# WebView2.ExecuteScriptAsync()のいくつかの使い方とDebug方法

C# 外部ライブラリを使わずに半角→全角カタカナ変換