Blazor でリッチテキストエディターに画像ギャラリーを追加する

Blazor のサンプルの「Insert Gallery」ダイアログ。blog、products、team フォルダーと、アップロードされた 4 枚の写真が一覧表示され、1 枚が選択されている

RichTextEditor は JavaScript のエディターなので、Blazor Web App では JS 相互運用で作成・削除し、ギャラリーはコンポーネントと並ぶ通常の HTTP エンドポイントとやり取りします。このページでは Blazor に関わる部分を紹介します。エディターのスクリプトとオプションの置き場所、コンポーネントがエディターを作成してよいタイミング、エディターを削除する方法、そしてギャラリーのルートを Program.cs にどう置くかです。.NET 8 の Interactive Server レンダリングを使います。

App.razor のスクリプトとオプション

エディターのファイル、共通の rte-files.js、そして 3 つの小さな相互運用関数は、App.razor の <head> に置きます。このファイルは一度だけレンダリングされ、対話型コンポーネントによってレンダリングされるものではありません。

Razor · Components/App.razor
<link rel="stylesheet" href="richtexteditor/rte_theme_default.css" />
<script src="richtexteditor/rte.js"></script>
<script src="richtexteditor/plugins/all_plugins.js"></script>
<script src="rte-files.js"></script>
<script>
    // Called from Home.razor. The editor options stay in JavaScript, where they can also hold
    // functions (custom buttons, event handlers) that cannot be sent from .NET as JSON.
    window.rteBlazor = {
        create: function (host) {
            host.rte = new RichTextEditor(host, {
                toolbar: "full",
                galleryEndpoint: "/rte/gallery",
                maxUploadFileSize: 4000000 // same limit as the server, checked before the upload starts
            });
        },
        // Returned as bytes: .NET reads them as a stream, so documents over Blazor's 32 KB
        // SignalR message limit still arrive.
        getHtml: function (host) {
            return new TextEncoder().encode(host.rte.getHTMLCode());
        },
        destroy: function (host) {
            if (!host || !host.rte) return;
            // editor.destroy() exists only in newer editor builds
            if (typeof host.rte.destroy === "function") host.rte.destroy();
            host.rte = null;
            host.innerHTML = "";
        }
    };
</script>

galleryEndpoint を含むエディターのオプションは、C# から送るのではなく JavaScript で記述します。オプションには、カスタムボタンやイベントハンドラーなどの関数を含めることができ、関数は .NET から JSON として渡せないからです。getHtml については Blazor Server でエディターの HTML を読み取るで説明しています。

エディターの作成

Razor · Components/Pages/Home.razor
protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (!firstRender) return;
    await JS.InvokeVoidAsync("rteBlazor.create", host);
    created = true;
}

このページには @rendermode InteractiveServer が指定されており、Blazor はこれをプリレンダリングします。つまり、最初のレンダリングはブラウザーが接続されていない状態でサーバー上で行われます。このときは JS 相互運用を使えません。OnAfterRenderAsync は回線が接続された後にだけ実行され、firstRender によって、再レンダリングのたびではなく 1 回だけエディターが作成されるようにしています。マークアップ内の @ref="host" 要素は空の <div> です。Blazor はその中に何も書き込まないので、DOM をめぐってエディターと競合することはありません。

エディターの削除

Razor · Components/Pages/Home.razor
public async ValueTask DisposeAsync()
{
    if (!created) return;
    try { await JS.InvokeVoidAsync("rteBlazor.destroy", host); }
    catch (JSDisconnectedException) { } // the browser tab is already gone
}
  • created フラグ。これがないと、コンポーネントのプリレンダリング版を破棄するときに InvalidOperationException: JavaScript interop calls cannot be issued at this time がスローされました。
  • JSDisconnectedException は、ユーザーがタブを閉じたときに発生します。ブラウザーがなくなった後でコンポーネントが破棄されるためです。
  • destroy() がない場合があります。テストしたリリース版のエディターのビルドには editor.destroy() がないため、rteBlazor.destroy はそれが存在する場合にだけ呼び出し、その後ホスト要素を空にします。

ギャラリーのルート

C# · Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
var app = builder.Build();

app.UseStaticFiles();
app.UseRteStaticFiles();   // /uploads, /richtexteditor, /rte-files.js
app.UseAntiforgery();      // Blazor's own forms; the /rte endpoints opt out (RteEndpoints.cs)

app.MapRteFiles();         // GET+POST /rte/gallery, POST /rte/upload
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();

app.Run();

MapRteFiles() は、GET と POST /rte/gallery、そして POST /rte/upload を最小 API のエンドポイントとして追加します。ギャラリーの一覧は、ASP.NET Core のギャラリーガイドと同じ JSON です。ルートは MapRazorComponents より前にマップし、POST ルートでは .DisableAntiforgery() を呼び出します。そうしないと、テンプレートの app.UseAntiforgery() がエディターのリクエストを拒否してしまうからです。ファイルを回線経由で送らない理由については、Blazor でリッチテキストエディターから画像をアップロードするで説明しています。

現在のダイアログの動作

リリース版のダイアログには「Accepted formats: JPG, JPEG, JFIF, PNG, GIF, WEBP, BMP, AVIF」と表示されます。サーバーが受け付けるのはそのうち 5 つなので、.bmp は送信された後 400 で拒否され、ダイアログはサーバーのメッセージではなく、独自の「Upload failed for pic.bmp.」を表示します。

テストする

このガイドのサンプルプロジェクトを dotnet run(.NET 8 SDK)で実行し、http://localhost:5304 を開きます。ツールバーが表示されるのを待ってから「Insert Gallery」をクリックします。ページ上部の画像のように、アプリの uploads/ フォルダー以下のフォルダーと画像が表示されます。一覧はシェルからも取得できます。

Shell
curl "http://localhost:5304/rte/gallery?folder=team"
{"currentFolder":"team","currentFolderDisplay":"/team","parentFolder":"","folders":[],"images":[]}

ほかのプラットフォーム向けの同じガイド

JavaScript · Next.js · PHP · ASP.NET Core · Razor Pages · MVC 5 · Web Forms

次に読む