Eine Bildergalerie für einen Rich-Text-Editor in Blazor

Der Dialog „Insert Gallery“ des Blazor-Beispiels mit den Ordnern blog, products und team und vier hochgeladenen Fotos, eines davon ausgewählt

RichTextEditor ist ein JavaScript-Editor. In einer Blazor Web App wird er daher über JS-Interop erstellt und entfernt, und die Galerie kommuniziert mit gewöhnlichen HTTP-Endpoints neben Ihren Komponenten. Diese Seite zeigt die Blazor-Teile: wohin die Skripte und Optionen des Editors gehören, wann die Komponente den Editor erstellen darf, wie er wieder entfernt wird und wie die Routen der Galerie in Program.cs eingebunden sind. Sie verwendet Interactive-Server-Rendering in .NET 8.

Skripte und Optionen in App.razor

Die Dateien des Editors, das gemeinsame rte-files.js und drei kleine Interop-Funktionen gehören in den <head> von App.razor, das einmal gerendert wird und nicht von einer interaktiven Komponente:

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>

Die Editor-Optionen mit galleryEndpoint werden in JavaScript geschrieben, statt aus C# gesendet zu werden. Optionen können Funktionen enthalten, etwa eigene Schaltflächen und Ereignishandler, und die lassen sich nicht als JSON aus .NET übertragen. getHtml wird in Das HTML des Editors in Blazor Server lesen erklärt.

Den Editor erstellen

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

Die Seite hat @rendermode InteractiveServer, und Blazor rendert sie vorab: Das erste Rendern findet auf dem Server statt, ohne verbundenen Browser. JS-Interop ist dann nicht möglich. OnAfterRenderAsync läuft erst, sobald die Circuit verbunden ist, und firstRender stellt sicher, dass der Editor einmal erstellt wird und nicht bei jedem erneuten Rendern. Das Element @ref="host" im Markup ist ein leeres <div>; Blazor schreibt nie hinein und kommt dem Editor im DOM daher nicht in die Quere.

Ihn entfernen

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
}
  • Das Flag created. Ohne dieses Flag warf das Disposen der vorab gerenderten Kopie der Komponente InvalidOperationException: JavaScript interop calls cannot be issued at this time.
  • JSDisconnectedException tritt auf, wenn der Benutzer den Tab schließt: Die Komponente wird disposed, nachdem der Browser schon weg ist.
  • destroy() fehlt möglicherweise. Der ausgelieferte Editor-Build, den wir getestet haben, hat kein editor.destroy(), daher ruft rteBlazor.destroy es nur auf, wenn es existiert, und leert dann das Host-Element.

Die Routen der Galerie

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() fügt GET und POST /rte/gallery sowie POST /rte/upload als Minimal-API-Endpoints hinzu. Die Liste der Galerie ist dasselbe JSON wie in der Galerie-Anleitung für ASP.NET Core. Die Routen werden vor MapRazorComponents abgebildet, und die POST-Routen rufen .DisableAntiforgery() auf, weil app.UseAntiforgery() aus der Vorlage die Requests des Editors sonst ablehnen würde. Bilder aus einem Rich-Text-Editor in Blazor hochladen erklärt, warum die Dateien nicht über die Circuit gehen.

Was der Dialog derzeit tut

Der ausgelieferte Dialog nennt „Accepted formats: JPG, JPEG, JFIF, PNG, GIF, WEBP, BMP, AVIF“. Der Server akzeptiert fünf davon, also wird eine .bmp gesendet, mit einem 400 abgelehnt, und der Dialog zeigt seine eigene Meldung „Upload failed for pic.bmp.“ statt der Meldung des Servers.

Testen

Starten Sie das Beispielprojekt zu dieser Anleitung mit dotnet run (.NET 8 SDK), öffnen Sie http://localhost:5304, warten Sie auf die Symbolleiste und klicken Sie auf Insert Gallery. Sie sehen die Ordner und Bilder unter dem Ordner uploads/ der App, wie im Bild oben. Die Liste antwortet auch in einer Shell:

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

Dieselbe Anleitung für andere Plattformen

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

Weiterlesen