C# WebView2を通じてWebサーバーなしでJavaScriptからローカルファイルを読み書き
TL;DR
Webサーバーを使用せずに、file:///
でブラウザーで開かれたhtmlファイルは、JavaScriptからPCのファイルシステムにアクセスできない。
WebView2コントロールにhtmlファイルを読み込み、JavaScriptがメッセージでWebView2に指示すれば、.NetのSystem.IO
を利用してPCファイルの読み書きが可能になる。
目次
- 完成イメージ
- わざわざC#とJavaScriptを組ませた理由
- WebView2とは
- Step 1. プロジェクト作成
- Step 2. page.htmlを作成
- step 3. page.jsを作成
- step 4. C#アプリ側を実装
- 参考:Debug方法
- 後書き
完成イメージ
このアプリ例は以下の3つの部分に構成されている:
- C#で書いたWinFormsアプリ
→ Formに含まれるControlはWebView2
のみ - Page.html
→ 上記WebView2のソースに指定 - 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. プロジェクト作成
- Visual Studio 2019側:
Windows Forms App/.Net 5.0プロジェクト - Visual Studio Code側:
htmlとjs或いはtsファイルを含むフォルダー
Step 2. page.htmlを作成
page.html
:textarea
ひとつと、"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
を検索してインストール
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ライブラリの莫大な機能が使えて、新たなドアが開いた感じがします。
今後アプリを作成するときに、ひとつの選択肢として置いておきたいと思います。
返信削除失礼します。
本記事についての質問等は可能でしょうか?
よろしくお願いいたします。
ご返信遅くなり失礼しました。ご質問どうぞ。
削除はじめまして!
削除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 デバッグ環境も記事と同じです。
解決策をお教えいただければと存じます。
よろしくお願いいたします。
こんばんは、ご質問を拝見しました。
削除まず、例外が発生した関数は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側で改良等は行われていないでしょうか?
さっそくの返信、ありがとうございます。
削除※1 js側を調べてDebugしてみます。
※2 興味深い記事と情報の提供、感謝しております & また何かありましたらこりずにお願いいたします。
追記:
返信削除例外発生個所を載せておきます。
return JsonSerializer.Deserialize(input, options);
プログラム実行時に、上記コードで例外が発生します!!
どうぞよろしくお願いいたします。
追記2:
返信削除inputの値は、下記のようにpage.htmlへのパスを入れています。
input = "\"file:///C://Users//Hoge//NodejsWebApp1222//page.html\""
以上です。
追記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;
}
};