Die deklarative Oberfläche (schreiben ohne new)Ein Bildschirm besteht aus gewöhnlichen C#-Ausdrücken: keine Markup-Sprache, keine Builder-Zeremonie und kein new:
public override VisualNode Build(ComponentContext context) =>
Column(gap: Space.S4, children: [
Text($"Count: {_count}", TypeRole.Display, context.Theme.TextPrimary),
Row(gap: Space.S3, children: [
Button("Up", onPressed: () => SetState(() => _count++)),
Button("Reset", Variant.Outline, onPressed: () => SetState(() => _count = 0)),
Jeder Name dort ist eine Factory-Methode, benannt genau wie der Typ, den sie zurückgibt. Es gibt keinen Import zu schreiben: Das SDK bringt die Oberfläche des Frameworks in jeder Datei Ihres Projekts in Reichweite, und Ihre eigenen Komponenten treten ihr automatisch bei (siehe unten).
Weil Styles typisierte Werte sind und keine CSS-Strings, prüft der Compiler die ganze Benutzeroberfläche, Layout und Styling eingeschlossen, und dieselbe Klasse rendert im Web und nativ über Photon. •
Eine Factory ist genau wie ihr Typ benannt. Column, Text, Button.
•
Sie spiegelt einen Konstruktor Parameter für Parameter (gleiche Namen, gleiche Reihenfolge, gleiche Standardwerte), sodass benannte Argumente unverändert zwischen new X(…) und X(…) übertragen werden.
•
Nach den gespiegelten Parametern darf eine Factory einen optionalen semantischen Schwanz nehmen: Parameter, die je einer öffentlichen init-only-Eigenschaft des Knotens entsprechen (gleicher Name, gleicher Typ) und über einen Objektinitialisierer angewandt werden. So sagt ein deklarativer Bildschirm, was Konstruktoren absichtlich nicht mitführen: `Pressable(child, onPressed, label:, selected:, disabled:, pressedBackground:, expanded:), Link(destination, child, label:, current:) und TextEntry(value, onChanged, label:, placeholder:, disabled:, obscure:). Die Maschinerie für Komposita und Modale (Role, Mixed, InitialFocus`) bleibt absichtlich nur im Initialisierer: Sie gehört den Komponenten, die diese Muster besitzen.
•
Container-Knoten nehmen einen abschließenden children-Parameter, geschrieben als Collection-Ausdruck.
•
Es gibt keine Überladungen. Die Oberfläche transpiliert zu einem JavaScript-Zwilling, und JS-Methoden können nicht überladen, also hat jeder Typ EINE kanonische Factory.
Layout sind Parameter, kein Initialisierer
Seit 0.2.0-preview.13
Row(gap: Space.S3, main: MainAlign.SpaceBetween, cross: CrossAlign.Start, children: [ … ])
Column(gap: Space.S2, wrap: true, runGap: Space.S4, children: [ … ])
Text("42", TypeRole.Display, align: TextAlignment.Center, tabular: true)
Semantik sind auch Parameter
Seit 0.2.0-preview.45
Ein Button nur mit Icon, den VoiceOver benennen kann, ein Navigationseintrag, der SAGT, dass er ausgewählt ist, der Link auf die Seite, auf der die Leserin ist: alles erreichbar, ohne die Factory-Form zu verlassen:
Pressable(Icon(Icons.Search), onPressed: Search, label: "Search", selected: true)
Link("/scan", Icon(Icons.Home), label: "Home", current: true)
TextEntry(_password, OnPassword, label: "Password", obscure: true)
Ein Control, das gedrückt wird, während die Tastatur bleibt, wo sie ist, der Button einer Formatierungsleiste neben dem Text, den er formatiert, oder eine Reihe Vorschläge unter einem Feld, sagt das mit canRequestFocus: false: Der Druck läuft, und der Fokus bleibt, wo er war.
Pressable(Icon(Icons.Check), onPressed: Apply, label: "Apply", canRequestFocus: false)
Seit 0.2.0-preview.61
Layout sind auch Parameter
Ein Container, der sein Elternteil füllt (das häufigste Layout überhaupt), braucht kein new mehr: Column, Row, Grid, Stack, ScrollView und ListView nehmen width und height vor children (ein SizeValue; sie auszulassen heißt Hug, genau das, was ein Initialisierer ohne sie bedeutet), und Pressable nimmt seine zusammengesetzte role. Von einer echten Anwendung nach der Migration ihrer ganzen Oberfläche gezählt: 49 Stellen, die wegen einer Breite imperativ geblieben waren, 3 wegen einer Rolle.
Row(gap: Space.S2, width: SizeValue.Fill, children: [ … ])
Column(height: SizeValue.Fill, children: [ ScrollView(list, height: SizeValue.Fill) ])
Pressable(Text("Overview"), Select, selected: true, role: PressableRole.Radio)
Seit 0.2.0-preview.46
Und die Einstellungs-Controls
Drei weitere aus demselben Bericht, und dieselbe Regel. Ein Switch kündigt sich an und wird untätig, während ein Stapel angewandt wird; eine Checkbox wird ebenfalls untätig; und SegmentedControl hatte überhaupt keine Factory, also blieb eine Tab-Leiste wegen eines einzigen Knopfes imperativ.
Switch(on: item.Enabled, onChanged: Toggle, label: item.Name, disabled: applying)
Checkbox(includeSystem, onChanged: Flip, label: "Include system files", disabled: scanning)
SegmentedControl(["Apps", "Login items"], selected, onChanged: Pick, stretch: false)
Eine Liste von Schaltern ohne Namen ist für assistive Technik eine Liste identischer Controls, und darum ist label der erste von ihnen.
Seit 0.2.0-preview.46
Ein Link braucht einen Ort zum Ankommen
Bookmark benennt eine Stelle in einem Bildschirm, die ein Link erreichen kann: Link("#features", …) kommt an dem Knoten an, der Bookmark = "features" sagt.
Box(SectionStyle, children: [ … ]) with { Bookmark = "features" }
Es ist absichtlich nicht Key, und die Verwechslung kostete eine Website ihre ganze Landing-Navigation plus die "Auf dieser Seite"-Schiene ihrer Docs: Ein Key ist Reconciler-Identität, eindeutig nur unter GESCHWISTERN, und in einer Liste ist er typischerweise "1", "2", "3", und das auf einen bildschirmweiten Namen zu projizieren kollidiert bei der ersten wiederholten Liste. Ein Knoten kann beides tragen.
Nichts validiert den Wert über die Leerheit hinaus, mit Absicht: Bookmarks werden häufig GENERIERT (eine Docs-Schiene bildet Slugs aus den Überschriften, die sie findet), und ein Duplikat zur Build-Zeit abzulehnen würde eine Seite scheitern lassen, weil die Quelle, die sie rendert, eine wiederholte Überschrift bekam.
Im Web wird es zur id des Elements, und der Realisierer gibt ihm Platz zum Landen: Ein Browser scrollt ein Ziel ganz nach oben, also käme es unter einem klebenden Header verdeckt an. Der Versatz wird aus dem Klebenden gemessen, das die Anwendung schon deklariert hat, nie aus einer Zahl, die in der Seite wiederholt wird: Eine Website hat eine 60dp-Navigation auf ihrer Landing und eine 56dp-Topbar in ihren Docs und nennt keine von beiden. Auf Photon ist es heute untätig: Ein Bookmark ist ein Ort, zu dem gescrollt wird, und programmatisches Scrollen liegt noch voraus.
Seit 0.2.0-preview.46
Weil Stack zwei Knöpfe vor children bekam, wird sein children benannt, Stack(Alignment.Center, children: [ … ]), die Regel, der jeder andere Container schon folgte.
Row und Column nehmen main, cross, wrap, runGap und padding; Text nimmt align, mono, tabular und styleOverride. Das waren init-only-Eigenschaften, eine davon zu setzen hieß also ein Objektinitialisierer, was new heißt, und new ist genau das, was diese Oberfläche entfernt. Eine Reihe, die ihren Inhalt zentrieren musste, fiel ganz aus der Oberfläche heraus und musste auf die alte Art geschrieben werden.
Breite, Höhe, Hintergrund und Eckenradius sind absichtlich keine Parameter an einem Flex: Ein Flex, das sie trägt, ist eine Box, die ein Flex umhüllt, und die Eigenschaften sagen das schon.
styleOverride an Text ist der Ausweg aus einer geschlossenen Typskala. Die Stufen sind die richtige Voreinstellung, und ein Design, das überall über sie hinausgreift, hat aufgehört, eine Skala zu haben, aber eine geschlossene Skala ohne Ausweg wird umgangen, indem ein rohes HtmlElement geschachtelt wird, was schlimmer ist, weil es nur auf einem Ziel funktioniert.
BRECHEND IN 0.2.0-PREVIEW.13 children steht am Ende (der Container-Kontrakt), die Knöpfe sitzen also zwischen ihm und gap: Column(Space.S3, [ … ]) wird Column(Space.S3, children: [ … ]). Ein Wort, und es ist schon die Normalform der Oberfläche.
Seltenere init-Eigenschaften behalten die Form Konstruktor + Initialisierer; die Factories sind Zucker über denselben Typen, nie eine zweite API:
Box(new BoxStyle { Padding = EdgeInsets.All(Space.S4), Background = theme.Surface },
Text("Still the same Box", TypeRole.BodyM))
Wert-Records (GridTrack, DialogAction, NavItem) haben absichtlich keine Factories: Sie sind Daten, und ein zielgetyptes new(…) liest sich schon gut.
Ihre eigenen Komponenten treten ihr bei
Definieren Sie eine Komponente; der Build generiert ihre Factory. Nichts zu registrieren, nichts zu importieren:
// Components/StatTile.cs
public sealed class StatTile : StatelessComponent
public StatTile(string label, string value) { Label = label; Value = value; }
public string Label { get; init; }
public string Value { get; init; }
public override VisualNode Build(ComponentContext context) => /* … */;
// Pages/HomePage.cs, kein using, kein new
Row(gap: Space.S3, children: [
StatTile("Count", $"{_count}"),
StatTile("Doubled", $"{_count * 2}"),
Ein Source-Generator liest die Kompilierung, findet die Komponenten und schreibt eine statische Klasse AppUI plus das global using static, das sie in Reichweite bringt. Er bleibt im Takt mit Ihren Komponenten, weil er aus ihnen generiert wird.
Seiten bekommen keine Factory. Eine [Page] wird über ihre Route erreicht, nie von Hand komponiert.
Welcher Konstruktor gespiegelt wird
Der breiteste, dieselbe Regel, die der Transpiler anwendet, wenn er Konstruktor-Überladungen zusammenfasst, damit Factory und ausgegebener Konstruktor sich nie widersprechen. Wenn der breiteste nicht der ist, den Sie anbieten wollen, wählen Sie einen anderen:
public sealed class Badge : StatelessComponent
public Badge(string label) { … } // ← dieser bekommt die Factory
public Badge(string label, int count, bool dot) { … }
EQ3101
Fehler
Zwei Konstruktoren einer Komponente sind mit [UiFactory] markiert. Eine Wahl braucht einen einzigen Sieger.
EQ3102
Warnung
Zwei Komponenten teilen einen Namen, also kann nur eine diese Factory besitzen. Benennen Sie eine um, oder bauen Sie die andere mit new.
<EQuanticGenerateFactorySurface>false</EQuanticGenerateFactorySurface>
Ihre Komponenten komponieren dann mit new, genau wie vorher.
Die eine scharfe Kante: Eine Factory verdeckt ihren Typ
Eine Methode, die wie ein Typ benannt ist, verdeckt diesen Typ in jeder Datei, in der die Oberfläche in Reichweite ist. Das beißt nur bei Typen, die über ein using ankommen, also bei denen des Frameworks, nicht bei Ihren:
Spacer.Fixed(34) // ✗ CS0119: `Spacer` bindet an die Factory-Methode
Panel.Empty("x") // ✓ Ihr eigener Typ, in Ihrem eigenen Namespace deklariert, gewinnt
C# löst Namen, die im aktuellen Namespace deklariert sind, vor using static-Importen auf, Ihre eigenen Komponenten werden also nie von ihren eigenen Factories verdeckt, und Sie brauchen den Umweg unten nie.
Die eigenen Statics des Frameworks, die hinter einem Factory-Namen liegen, bekommen eine Factory unter einem eigenen Namen:
Badge.AsDot(variant)
DotBadge(variant)
new Icon(packGlyph)
Glyph(packGlyph)
Seit 0.2.0-preview.7 (die Glyph-Zeile; die anderen zwei seit 0.2.0-preview.2)
Ein Konformitätstest geht jede Factory ab, sucht nach Statics auf dem Typ, den sie verdeckt, und scheitert, indem er sie benennt, bis jeder eine benannte Factory hat, sodass ein vierter nicht unbemerkt auftauchen kann.
Glyph ist aus einem etwas anderen Grund da als die anderen zwei, und es lohnt zu wissen, aus welchem: Icon hat zwei Konstruktoren, einen, der das kuratierte Icons-Enum des Frameworks nimmt, einen, der das IconGlyph nimmt, das ein Icon-Paket herausgibt. Das gespiegelte Icon(...) kann nur einer von beiden sein, und Überladungen gibt es hier nicht, also hatte eine Pack-Glyphe überhaupt keinen Weg in eine Datei, die diese Oberfläche importiert: Sie konnte nur mit new gezeichnet werden, an der einen Stelle, an der das Framework Ihnen verspricht, dass Sie es nie brauchen.
using eQuantic.UI.MaterialSymbols;
Glyph(MaterialSymbolsIcons.PlayArrowRounded) // der Katalog jedes Icon-Pakets
Icon(Icons.Check) // der kuratierte Satz
Wie das den Browser erreicht
Die generierte Oberfläche wird auf die Platte geschrieben (EmitCompilerGeneratedFiles), weil eqc Dateien liest, nicht die C#-Kompilierung: Eine Factory, die Ihre Seite aufruft, muss Teil dessen sein, was der Transpiler sieht, sonst löst der Aufruf sich zu nichts auf und das ausgegebene JavaScript verfällt stillschweigend. Sie wird dann wie jede andere Klasse zu einem Modul transpiliert, also existiert AppUI.statTile(…) im Bundle neben UI.column(…).
DIE RESTE EINES GENERATORS GEHEN MIT IHM Der Compiler entfernt keine generierte Datei, also würde eine Komponente, die Sie löschen, oder ein Generator, den Sie umbenennen oder entfernen, seine letzte Ausgabe für eqc zum Lesen zurücklassen: ein Phantomtyp oder eine zweite Kopie eines lebenden. Das SDK entfernt sie selbst: Nach einer Kompilierung, die läuft, wird eine generierte Datei gelöscht, die diese Kompilierung nicht geschrieben hat, und das Build-Log benennt sie. Ein dotnet clean ist nicht nötig.
•
Design System: die typisierten Tokens, aus denen die Argumente gemacht sind