Das System zur Asset-VerwaltungeQuantic.UI bietet ein deklaratives System für Asset-Abhängigkeiten, mit dem Komponenten die Skripte und Stylesheets deklarieren, die sie brauchen. Die Assets werden beim SSR automatisch gesammelt, dedupliziert und in den <head> der Seite injiziert.
Das System löst ein häufiges Problem: Komponenten, die von externen Bibliotheken abhängen (Prism.js, Chart.js usw.), brauchen ihre Skripte und ihr CSS geladen, sollten aber keine <script>-Tags inline in ihr gerendertes HTML einbetten. Stattdessen deklarieren sie Abhängigkeiten, und das Framework übernimmt die Injektion.
Gang durch den Komponentenbaum → IRequireAssets sammeln → IComponentAssetProvider<T> sammeln → Deduplizieren → In <head> injizieren
Alle Typen liegen in eQuantic.UI.Server.Assets.
Die Basisschnittstelle aller Asset-Typen.
string Key { get; } // Eindeutiger Schlüssel für die Deduplizierung
string? Id { get; } // Optionale HTML-Id, für Manipulation auf dem Client
string Render(); // Rendert als HTML-Tag
Typ
Ausgabe
Format des Schlüssels
ScriptAsset
<script src="..." defer></script>
script:{Src}
InlineScriptAsset
<script>...</script>
inline-script:{hash}
StylesheetAsset
<link rel="stylesheet" href="...">
stylesheet:{Href}
InlineStyleAsset
<style>...</style>
inline-style:{hash}
Alle Typen nehmen einen optionalen Id-Parameter für die DOM-Manipulation auf dem Client:
new StylesheetAsset("https://cdn.example.com/theme.css", Id: "theme-css")
// Rendert: <link rel="stylesheet" href="https://cdn.example.com/theme.css" id="theme-css">
Der fließende Builder, der Komponenten übergeben wird, um Abhängigkeiten zu deklarieren:
public class AssetBuilder
AssetBuilder AddScript(string src, bool defer = true, string? id = null);
AssetBuilder AddInlineScript(string content, string? id = null);
AssetBuilder AddStylesheet(string href, string? id = null);
AssetBuilder AddInlineStyle(string content, string? id = null);
Sammelt und dedupliziert die Assets. Die Renderreihenfolge ist optimiert: CSS zuerst, dann JS (die richtige Reihenfolge für das Blockieren des Renderns).
StylesheetAsset → InlineStyleAsset → ScriptAsset → InlineScriptAsset
Die Deduplizierung nutzt TryAdd mit dem Key des Assets, die erste Registrierung gewinnt also.
Muster 1: IRequireAssets (selbst deklarierend)
Für Komponenten, die ihre Abhängigkeiten besitzen. Die Komponente selbst deklariert, was sie braucht.
public class CodeBlock : StatelessComponent, IRequireAssets
public void ConfigureAssets(AssetBuilder assets)
"https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css",
assets.AddScript("https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js");
Wann man es nutzt:
•
Framework-Komponenten (CodeBlock, Diagramme usw.)
•
Komponenten, deren Abhängigkeiten wesensmäßig dazugehören
•
Wenn wer die Komponente nutzt, von den darunterliegenden Bibliotheken nichts wissen muss
Muster 2: IComponentAssetProvider\<T\> (externer Anbieter)
Um Assets einer Komponente von außen zuzuordnen, typischerweise bei Fremdkomponenten, die IRequireAssets nicht implementieren.
public class ChartJsAssetProvider : IComponentAssetProvider<ChartCanvas>
public void ConfigureAssets(AssetBuilder assets)
assets.AddScript("https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js");
Registrierung (wählen Sie eine):
// Variante 1: automatischer Scan (werden in den gescannten Assemblys selbst gefunden)
options.ScanAssembly(typeof(Program).Assembly);
// Variante 2: ausdrückliche Registrierung über UIOptions
options.WithAssetProvider<ChartJsAssetProvider>();
// Variante 3: manuelle Registrierung in der DI
services.AddSingleton<IComponentAssetProvider<ChartCanvas>, ChartJsAssetProvider>();
Wann man es nutzt:
•
Fremdkomponenten, die Sie nicht ändern können
•
Überschreibungen auf Anwendungsebene für die Assets einer Framework-Komponente
•
Komponenten aus externen Paketen
Wenn beide Muster für denselben Komponententyp existieren:
1.
IRequireAssets läuft zuerst (die Voreinstellung der Komponente)
2.
IComponentAssetProvider<T> läuft danach (der externe Anbieter)
Die Deduplizierung geschieht nach Key, und der erste gewinnt. Wenn der Anbieter das Standard-Asset einer Komponente überschreiben soll, nutzen Sie einen anderen Key (etwa eine andere URL).
Wie es funktioniert (die SSR-Pipeline)
Beim serverseitigen Rendern geht ServerRenderingService den Komponentenbaum ab:
├── Erzeugt die AssetCollection
├── CollectAssets(rootComponent, assets, services, visited)
│ ├── Prüft IRequireAssets → ConfigureAssets()
│ ├── Prüft die DI auf IComponentAssetProvider<T> → ConfigureAssets()
│ ├── Rekursion in component.Children
│ └── Für eine StatelessComponent → Build() → Rekursion auf dem Ergebnis
└── Gibt ein ServerRenderResult mit den Assets zurück
Die Assets werden dann in HtmlShellOptions.HeadTags verschmolzen, bevor die Seite ausgeliefert wird.
Wichtige Verhaltensweisen:
•
Deduplizierung je Typ: Jeder Komponententyp wird einmal verarbeitet (über HashSet<Type>)
•
Deduplizierung je Asset: Jeder Asset-Schlüssel wird einmal registriert (über Dictionary.TryAdd)
•
Null Mehraufwand: Seiten ohne Komponenten, die Assets verlangen, bekommen keine zusätzlichen Tags
•
Nachsichtiger Rückfall: Scheitert Build() (etwa wegen fehlender DI), wird die Komponente übersprungen
Automatische Registrierung
Implementierungen von IComponentAssetProvider<T> werden während AddUI() selbst gefunden:
builder.Services.AddUI(options =>
options.ScanAssembly(typeof(Program).Assembly); // Findet die Anbieter hier
Der Scan sucht in den gescannten Assemblys alle nicht abstrakten Klassen, die IComponentAssetProvider<T> implementieren, und registriert sie über TryAddSingleton als Singletons.
Ausdrückliche Registrierungen über WithAssetProvider<T>() haben Vorrang vor automatisch gescannten (sie werden zuerst registriert).
Beispiel aus der Praxis: CodeBlock
Die Komponente CodeBlock zeigt das ganze Muster:
public class CodeBlock : StatelessComponent, IRequireAssets
public void ConfigureAssets(AssetBuilder assets)
// Stylesheet mit Id, für dynamisches Umschalten des Themes
"https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css",
// Die Kernskripte von Prism.js
assets.AddScript("https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js");
assets.AddScript("https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js");
// Hilfsfunktionen + automatisches Umschalten zwischen hell und dunkel
"function copyToClipboard(id){...}" +
"function toggleCodeBlock(id){...}" +
"function updateTheme(){...}" + // Schaltet zwischen prism.css und prism-tomorrow.css
"new MutationObserver(function(){updateTheme()})" +
".observe(document.documentElement,{attributes:true,attributeFilter:['class']});" +
Wer entwickelt, nutzt einfach new CodeBlock(code, "csharp"), und die Skripte von Prism.js, das CSS, das Umschalten des Themes und die Hilfsfunktionen werden alle automatisch erledigt.
Seit 0.2.0-preview.1
Das Anwendungs-Icon, in C# gezeichnet (IAppIcon)
Das Launcher-Icon ist eine Komponente wie alles andere, im selben Vokabular, es kann also nicht von der Marke abdriften, mit der die Anwendung schon zeichnet:
public sealed class AppIcon : IAppIcon
public VisualNode Build(ComponentContext context) =>
Box(new BoxStyle { Background = Brand, CornerRadius = new CornerRadii(0) },
Text("eQ", TypeRole.Display, Ink).Centered());
Zwei Zäune, beide vom Medium und nicht vom Framework:
•
Füllen Sie das ganze Quadrat, deckend. Ein transparentes Icon wird gegen das zusammengesetzt, was der Launcher gerade zeigt, und das ist nie das, wogegen Sie entworfen haben.
•
Kein Zustand, keine Interaktion. Es wird einmal gebaut; ein Druck-Handler auf einem Icon ist ein Handler, den niemand erreichen kann. Bleiben Sie bei Formen, Gradienten und höchstens einem oder zwei Buchstaben, denn in der Größe, in der ein Startbildschirm es wirklich zeigt, ist alles Feinere ein Fleck.
Der Build rastert es in jede Größe, die jede Plattform will, und verlinkt die für das Web im Head, ohne dass die Anwendung es sagt.