La surface déclarative (écrire sans new)Un écran, ce sont de simples expressions C# : pas de langage de balisage, pas de cérémonie de builder, et pas de 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)),
Chaque nom là-dedans est une méthode factory nommée exactement comme le type qu'elle renvoie. Il n'y a aucun import à écrire : le SDK met la surface du framework à portée dans chaque fichier de votre projet, et vos propres composants la rejoignent automatiquement (voir plus bas).
Comme les styles sont des valeurs typées et non des chaînes CSS, le compilateur vérifie toute l'interface, layout et styles compris, et la même classe se rend sur le web et en natif via Photon. •
Une factory est nommée exactement comme son type. Column, Text, Button.
•
Elle reflète un constructeur paramètre pour paramètre (mêmes noms, même ordre, mêmes valeurs par défaut), si bien que les arguments nommés passent entre new X(…) et X(…) sans changement.
•
Après les paramètres reflétés, une factory peut prendre une queue sémantique facultative : des paramètres qui correspondent chacun à une propriété publique init-only du nœud (même nom, même type) et qui sont appliqués via un initialiseur d'objet. C'est ainsi qu'un écran déclaratif énonce ce que les constructeurs ne portent délibérément pas : `Pressable(child, onPressed, label:, selected:, disabled:, pressedBackground:, expanded:), Link(destination, child, label:, current:), et TextEntry(value, onChanged, label:, placeholder:, disabled:, obscure:)`. La mécanique de composition et des modales (Role, Mixed, InitialFocus) reste réservée à l'initialiseur à dessein : elle appartient aux composants qui possèdent ces motifs.
•
Les nœuds conteneurs prennent un paramètre children final, écrit comme une expression de collection.
•
Il n'y a pas de surcharges. La surface se transpile en un jumeau JavaScript, et les méthodes JS ne peuvent pas se surcharger, donc chaque type a UNE factory canonique.
Le layout, ce sont des paramètres, pas un initialiseur
Depuis 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)
La sémantique aussi, ce sont des paramètres
Depuis 0.2.0-preview.45
Un bouton à icône seule que VoiceOver peut nommer, un élément de navigation qui ÉNONCE qu'il est sélectionné, le lien vers la page où se trouve le lecteur, tout cela est atteignable sans quitter la forme factory :
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)
Un contrôle pressé pendant que le clavier reste où il est, le bouton d'une barre de mise en forme à côté du texte qu'il met en forme ou une rangée de suggestions sous un champ, le dit avec canRequestFocus: false : la pression s'exécute, et le focus reste où il était.
Pressable(Icon(Icons.Check), onPressed: Apply, label: "Apply", canRequestFocus: false)
Depuis 0.2.0-preview.61
Le layout aussi, ce sont des paramètres
Un conteneur qui remplit son parent (le layout le plus courant qui soit) n'a plus besoin de new : Column, Row, Grid, Stack, ScrollView et ListView prennent width et height avant children (un SizeValue ; les omettre vaut Hug, exactement ce que signifie un initialiseur sans eux), et Pressable prend son role composite. Compté par une véritable application après la migration de toute son interface : 49 endroits restés impératifs pour une largeur, 3 pour un rôle.
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)
Depuis 0.2.0-preview.46
Et les contrôles de réglages
Trois autres venus du même rapport, et la même règle. Un Switch s'annonce et devient inerte pendant qu'un lot s'applique ; une Checkbox devient inerte aussi ; et SegmentedControl n'avait aucune factory, si bien qu'une bande d'onglets restait impérative pour un seul bouton.
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)
Une liste d'interrupteurs sans noms est une liste de contrôles identiques pour une technologie d'assistance, et c'est pourquoi label est le premier d'entre eux.
Depuis 0.2.0-preview.46
Un lien a besoin d'un endroit où arriver
Bookmark nomme un endroit d'un écran qu'un lien peut atteindre : Link("#features", …) arrive au nœud qui dit Bookmark = "features".
Box(SectionStyle, children: [ … ]) with { Bookmark = "features" }
Ce n'est délibérément pas Key, et la confusion a coûté à un site toute la navigation de sa landing plus le rail "Sur cette page" de ses docs : une Key est une identité de réconciliateur, unique seulement entre VOISINS, et dans une liste elle vaut typiquement "1", "2", "3", et projeter cela sur un nom valable pour tout l'écran entre en collision dès la première liste répétée. Un nœud peut porter les deux.
Rien ne valide la valeur au-delà du vide, à dessein : les bookmarks sont fréquemment GÉNÉRÉS (un rail de docs fabrique le slug des titres qu'il trouve), et refuser un doublon au build ferait échouer une page parce que la source qu'elle rend a gagné un titre répété.
Sur le web il devient l'id de l'élément, et le réalisateur lui donne de la place pour atterrir : un navigateur fait défiler une cible tout en haut, donc sous un en-tête collant elle arriverait cachée. Le décalage est mesuré depuis le collant que l'application a déjà déclaré, jamais un nombre répété dans la page : un site a une nav de 60dp sur sa landing et une topbar de 56dp dans ses docs et n'en déclare aucune. Sur Photon c'est inerte aujourd'hui : un bookmark est un endroit où défiler, et le défilement programmatique reste à venir.
Depuis 0.2.0-preview.46
Comme Stack a gagné deux boutons avant children, son children devient nommé, Stack(Alignment.Center, children: [ … ]), la règle que tous les autres conteneurs suivaient déjà.
Row et Column prennent main, cross, wrap, runGap et padding ; Text prend align, mono, tabular et styleOverride. C'étaient des propriétés init-only, donc en fixer une voulait dire un initialiseur d'objet, ce qui veut dire new, et new est exactement ce que cette surface supprime. Une rangée qui devait centrer son contenu sortait entièrement de la surface et devait s'écrire à l'ancienne.
Largeur, hauteur, fond et rayon de coin ne sont délibérément pas des paramètres d'un flex : un flex qui les porte est une Box enveloppant un flex, et les propriétés le disent déjà.
styleOverride sur Text est la sortie d'une échelle typographique fermée. Les crans sont le bon défaut et un design qui les dépasse partout a cessé d'avoir une échelle, mais une échelle fermée sans sortie se contourne en imbriquant un HtmlElement brut, ce qui est pire, parce que cela ne marche que sur une cible.
CASSANT EN 0.2.0-PREVIEW.13 children est en dernier (le contrat des conteneurs), donc les boutons se placent entre lui et gap : Column(Space.S3, [ … ]) devient Column(Space.S3, children: [ … ]). Un mot, et c'est déjà la forme normale de la surface.
Les propriétés init plus rares gardent la forme constructeur + initialiseur ; les factories sont du sucre au-dessus des mêmes types, jamais une seconde API :
Box(new BoxStyle { Padding = EdgeInsets.All(Space.S4), Background = theme.Surface },
Text("Still the same Box", TypeRole.BodyM))
Les records de valeur (GridTrack, DialogAction, NavItem) n'ont délibérément aucune factory : ce sont des données, et un new(…) typé par la cible se lit déjà bien.
Vos propres composants la rejoignent
Définissez un composant ; le build génère sa factory. Rien à enregistrer, rien à importer :
// 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, pas de using, pas de new
Row(gap: Space.S3, children: [
StatTile("Count", $"{_count}"),
StatTile("Doubled", $"{_count * 2}"),
Un générateur de source lit la compilation, trouve les composants, et écrit une classe statique AppUI plus le global using static qui la met à portée. Il reste au pas de vos composants parce qu'il est généré à partir d'eux.
Les pages n'ont pas de factory. Une [Page] est atteinte par sa route, jamais composée à la main.
Quel constructeur est reflété
Le plus large, la même règle que le transpileur applique quand il réduit les surcharges de constructeur, pour que la factory et le constructeur émis ne se contredisent jamais. Quand le plus large n'est pas celui que vous voulez offrir, élisez-en un autre :
public sealed class Badge : StatelessComponent
public Badge(string label) { … } // ← celui-ci reçoit la factory
public Badge(string label, int count, bool dot) { … }
Code
Sévérité
Signification
EQ3101
Erreur
Deux constructeurs d'un composant portent [UiFactory]. Une élection a besoin d'un seul vainqueur.
EQ3102
Avertissement
Deux composants partagent un nom, donc un seul peut posséder cette factory. Renommez-en un, ou construisez l'autre avec new.
<EQuanticGenerateFactorySurface>false</EQuanticGenerateFactorySurface>
Vos composants se composent alors avec new, exactement comme avant.
Le seul angle vif : une factory masque son type
Une méthode nommée comme un type masque ce type dans tout fichier où la surface est à portée. Cela ne mord que sur les types qui arrivent par un using, c'est-à-dire ceux du framework, pas les vôtres :
Spacer.Fixed(34) // ✗ CS0119 : `Spacer` se lie à la méthode factory
Panel.Empty("x") // ✓ votre propre type, déclaré dans votre propre namespace, gagne
C# résout les noms déclarés dans le namespace courant avant les imports using static, donc vos propres composants ne sont jamais masqués par leurs propres factories, et vous n'avez jamais besoin du contournement ci-dessous.
Les statiques propres au framework qui se trouvent derrière un nom de factory reçoivent une factory sous un nom à eux :
Badge.AsDot(variant)
DotBadge(variant)
new Icon(packGlyph)
Glyph(packGlyph)
Depuis 0.2.0-preview.7 (la ligne de Glyph ; les deux autres depuis la 0.2.0-preview.2)
Un test de conformité parcourt chaque factory, cherche des statiques sur le type qu'elle masque, et échoue en les nommant jusqu'à ce que chacun ait une factory nommée, si bien qu'un quatrième ne peut pas apparaître sans qu'on le remarque.
Glyph est là pour une raison un peu différente des deux autres, et il vaut la peine de savoir laquelle : Icon a deux constructeurs, l'un prenant l'enum Icons curé du framework, l'autre prenant l'IconGlyph que distribue un paquet d'icônes. L'Icon(...) reflété ne peut être que l'un des deux, et il n'y a pas de surcharges ici, donc un glyphe de pack n'avait aucune entrée dans un fichier important cette surface : il ne pouvait être dessiné qu'avec new, au seul endroit où le framework vous promet que vous n'en avez jamais besoin.
using eQuantic.UI.MaterialSymbols;
Glyph(MaterialSymbolsIcons.PlayArrowRounded) // le catalogue de n'importe quel paquet d'icônes
Icon(Icons.Check) // l'ensemble curé
La surface générée est écrite sur le disque (EmitCompilerGeneratedFiles) parce qu'eqc lit des fichiers, pas la compilation C# : une factory que votre page appelle doit faire partie de ce que voit le transpileur, sinon l'appel ne résout rien et le JavaScript émis se dégrade en silence. Elle est ensuite transpilée en un module comme n'importe quelle autre classe, donc AppUI.statTile(…) existe dans le bundle à côté de UI.column(…).
LES RESTES D'UN GÉNÉRATEUR PARTENT AVEC LUI Le compilateur ne supprime aucun fichier généré, donc un composant que vous supprimez, ou un générateur que vous renommez ou retirez, laisserait sa dernière sortie derrière lui pour qu'eqc la lise : un type fantôme, ou une seconde copie d'un type vivant. Le SDK les retire lui-même : après une compilation qui tourne, un fichier généré que cette compilation n'a pas écrit est supprimé, et le log du build le nomme. Aucun dotnet clean n'est nécessaire.
•
Composants : le catalogue auquel correspond chaque factory