Repository navigation
Capabilities fr
🌐 Cette page en : English · Português · Español · Français · Deutsch
Ce que la machine sait faire (caméra, localisation, photos, biométrie, le réseau) sous forme de services pris par constructeur, réalisés par hôte. Un composant demande une capacité et n'apprend jamais quelle cible a répondu.
public sealed class ScanShell(ICamera? camera, ILocation? location) : StatefulComponent
{
private async void LocateAsync()
{
if (location is null) return; // cet hôte n'en a pas
var here = await location.GetCurrentAsync();
}
}Nullable, toujours. Un hôte qui ne sait pas faire quelque chose n'enregistre rien, donc le
service se résout à null et l'application montre ce qu'elle sait déjà montrer. C'est la réponse du
framework partout : rapporter l'absence plutôt que de faire semblant avec un stub qui échoue plus
tard, à un pire moment.
| Capacité | Ce qu'elle répond | Depuis |
|---|---|---|
IPhotoLibrary |
GetPermissionAsync(), PickImageAsync(), une image que la personne a choisie |
0.2.0-preview.1 |
ICamera |
StartPreviewAsync() pour une ICameraSession : une texture en direct, et son Capture() pour une image fixe |
0.2.0-preview.1 |
ILocation |
GetCurrentAsync(), et Subscribe(…) pour le flux des changements |
0.2.0-preview.1 |
IBiometrics |
IsAvailable, AuthenticateAsync(reason) : Face ID / Touch ID / celui de la plateforme |
0.2.0-preview.1 |
IMotionSensor |
Subscribe(…) pour les relevés de mouvement de l'appareil |
0.2.0-preview.1 |
INetworkStatus |
Current, et Subscribe(…) : la joignabilité et ce qui la porte |
0.2.0-preview.1 |
ITextClipboard |
Read() / Write(text), pour le bouton Copier propre à une page et ce par quoi un champ natif copie, coupe et colle. Chaque cible, téléphones compris |
0.2.0-preview.1 |
IThemeController |
La bascule clair/sombre. Voir ServerIntegration | 0.2.0-preview.1 |
IAppStorage / ISecretStore
|
Préférences et secrets durables. Voir Stockage | 0.2.0-preview.10 |
IClock |
Every(interval, onTick) : le temps lui-même, comme quelque chose à quoi un composant peut s'abonner |
0.2.0-preview.31 |
IFrameTicker |
OnFrame(tick) : un callback avant chaque frame, avec son temps et le delta, pour ce qu'un style ne peut pas exprimer |
0.2.0-preview.46 |
IUiDispatcher |
IsOnUiThread, Post(work) : le thread depuis lequel on peut toucher à une UI, et le chemin pour y arriver |
0.2.0-preview.46 |
IConsent |
State, Grant(), Deny() : la réponse de la visiteuse au suivi non essentiel, le cookie dont dépend l'installeur GTM. Voir Analytics
|
0.2.0-preview.46 |
IDeepLinks |
Launch, Subscribe(onOpened) : les URL AVEC lesquelles cette application est ouverte. Photon |
0.2.0-preview.47 |
IFileDialogs |
PickFile, PickFiles, PickFolder, PickSavePath : demander un fichier à une PERSONNE. Bureau |
0.2.0-preview.47 |
IWorkspace |
Reveal(path), OpenFile(path) : remettre un FICHIER au système. Bureau |
0.2.0-preview.49 |
IUrlLauncher |
OpenUrlAsync(url), CanOpen(url) : remettre un LIEN au système, selon les schémas que l'application ouvre. Photon, téléphones compris |
0.2.0-preview.64 |
IServerEvents |
Subscribe(topic, onEvent, onRefused), Connection, OnConnectionChanged(…) : ce que le serveur publie sur un sujet typé, sur une connexion par page. Web. Voir Événements serveur
|
0.2.0-preview.61 |
Depuis 0.2.0-preview.31
Un écran est construit, mis en page et dessiné sur UN seul thread : le thread principal de la
plateforme sur chaque cible native, et le seul thread qui existe dans un navigateur. Tout le reste
de ce que fait une application se passe ailleurs : un scanner qui parcourt un disque, un
téléchargement, une lecture en base de données. Quand l'un d'eux se termine il veut montrer ce qu'il
a trouvé, et SetState est le moyen.
C'était autrefois une course de données sans message d'erreur. SetState modifiait les champs du
composant depuis le thread qui l'appelait, quel qu'il soit, pendant que le thread de rendu lisait ce
même arbre : une frame dessinée à partir d'un état à moitié ancien, une collection énumérée pendant
qu'elle grandit, un plantage qui se reproduit une fois par semaine.
Rien à activer. SetState demande d'abord à IUiDispatcher. Hors du thread d'UI, la mutation
entière est mise en file et s'exécute au début de la frame suivante, avant que quoi que ce soit ne
soit construit ; sur lui, en ligne, exactement comme avant. Là où il n'y a pas d'autre thread il n'y
a rien à transférer, et la réponse honnête est que la capacité se déclare déjà sur le thread d'UI :
le navigateur a un seul thread, et un serveur rend une requête sur le thread qui l'a demandée.
Et la ligne qu'il ne franchit pas. Le dispatcher rend sûr l'état qui voyage à travers
SetState. L'état partagé qu'un thread de travail touche EN DEHORS de lui reste à l'application de
le protéger : un modèle que l'application élague sur un thread d'arrière-plan, une collection à
laquelle une tâche ajoute. Une application qui lirait cette capacité comme "le threading est réglé
maintenant" et supprimerait sa propre garde supprimerait la seule chose qui se tient entre la passe
de rendu et un arbre qu'on modifie sous elle. Ce que le dispatcher donne bien à cette garde, c'est
une garantie qui vaut la peine : le SetState qui LÈVE un drapeau s'exécute en ligne quand il vient
du thread d'UI, donc le drapeau est déjà levé avant que le travail ne commence.
Depuis 0.2.0-preview.46
IClock est délibérément périodique (des centaines de millisecondes et plus), et sa propre limite
dit pourquoi l'animation par frame n'a pas été offerte à travers lui. IFrameTicker est cet autre
contrat : un callback avant chaque frame, qui porte le temps de la frame et le delta depuis la
dernière de l'abonné, sur le thread d'UI et avant que l'arbre ne soit construit, si bien qu'un
SetState dans un tick est ce que cette frame même dessine.
protected override void OnMount() =>
_ticking = Ticker.OnFrame(tick => SetState(() => _phase += tick.DeltaMs / 1000f));
protected override void OnUnmount() => _ticking?.Dispose();Un abonnement MAINTIENT LE FLUX DES FRAMES, ce qui est le but et aussi le coût : libérez-le quand le mouvement s'arrête. Sur un serveur il n'y a pas de frames, donc s'abonner réussit et ne se déclenche jamais.
La plupart des mouvements n'ont besoin de rien de tout cela. Un changement de style glisse tout seul désormais (voir Photon) ; le ticker est pour ce qu'un style ne peut pas exprimer : de la physique, une simulation, une géométrie recalculée à chaque frame à partir de données qui ne s'arrêtent jamais.
Depuis 0.2.0-preview.46
Le acme://activate?key=… d'un e-mail de licence, un lien de retour depuis une connexion dans le
navigateur, une collègue qui colle un lien vers un écran. Déclarer le schéma, c'est
builder.Bundle.UrlScheme("acme") (voir
Photon) ; IDeepLinks, c'est l'application
qui LIT ce avec quoi elle a été ouverte. Sans les deux moitiés un lien profond ouvre l'application à
sa porte d'entrée et laisse tomber ce qu'il portait, ce qui pour la personne qui a cliqué ressemble
exactement à un lien cassé.
protected override void OnMount()
{
if (Links.Launch is { } opened) Activate(opened); // ce qui a DÉMARRÉ cette application
_link = Links.Subscribe(Activate); // et chacun à partir de maintenant
}
protected override void OnUnmount() => _link?.Dispose();Faites les deux, et voici pourquoi. Launch n'est pas lisible dans un constructeur sur macOS,
mesuré plutôt que supposé. L'URL de lancement arrive comme un AppleEvent, les AppleEvents sont
livrés PAR la run loop, et la run loop démarre après la construction du premier arbre, donc un
lancement à froid atteint un ABONNÉ et trouve la propriété encore nulle un instant plus tôt.
L'abonnement est ce qui attrape un lancement à froid ; la propriété sert à demander plus tard, sur
un écran construit après. Une application qui fait les deux a raison partout.
Launch n'est pas rejoué à un abonné, donc une application qui fait les deux agit une seule fois.
Une URL que l'application ne RECONNAÎT pas est livrée quand même : décider de ce que signifie un
hôte inconnu revient à l'application, et l'avaler reviendrait à décider à sa place sans laisser de
trace. Une chaîne qui n'est pas une URL est écartée.
Sur un téléphone (Depuis 0.2.0-preview.63) les deux mêmes lignes fonctionnent, et le même
builder.Bundle.UrlScheme("acme") est toute la déclaration : le build écrit le type d'URL qu'un
iPhone lit et le filtre de liens qu'Android compare, et personne n'ouvre un Info.plist ni un
manifeste.
- Un téléphone remet le lien AVEC le lancement, donc là
Launchest déjà renseigné quand le premier arbre est construit, et il est exact : une application ouverte depuis son icône répondnull, et un lien touché une heure plus tard est entendu par un abonné et ne devient pas cette valeur. - Un lien ouvert pendant que l'application tourne atteint l'application en cours d'exécution. Sur Android une application qui déclare un schéma est une tâche unique, donc le système lui remet le lien au lieu de démarrer une seconde copie par-dessus, et seul un lien sur un schéma que l'application a déclaré est livré, quel que soit celui qui l'a envoyé.
- Ce sont les schémas propres à l'application. Un lien
httpsqui ouvre l'application sans poser de question (Universal Links, App Links) a aussi besoin de la parole du site, et n'est pas encore là.
Photon seulement : sur le web un lien profond est la barre d'adresse, et le routeur en est déjà le propriétaire.
Depuis 0.2.0-preview.47
La seule façon dont une application en bac à sable touche à quoi que ce soit qu'on ne lui a pas
donné, et la raison pour laquelle PhotonEntitlements.UserSelectedFiles existe.
var folder = await Dialogs.PickFolder("Choose a folder to scan");
var report = await Dialogs.PickSavePath("report.csv",
filters: [new FileFilter("Spreadsheets", "csv")]);Null signifie que la personne a ANNULÉ, ce qui est une réponse ordinaire et non une exception :
quelqu'un qui ferme un dialogue n'a échoué à rien, et une application qui doit intercepter pour le
savoir est une application qui oubliera de le faire. PickFiles répond par une liste vide pour la
même raison : un appelant sur le point de boucler ne devrait pas avoir à demander deux fois.
Appelez depuis n'importe où. Les panneaux sont modaux et réservés au thread principal, et la
réalisation passe elle-même par IUiDispatcher, parce qu'un panneau ouvert depuis un thread de
travail ne s'ouvre pas du tout : il n'apparaît tout simplement jamais, sans rien dans le journal.
Un chemin vaut pour CETTE EXÉCUTION. Dans une application en bac à sable, l'autorisation qu'une personne a accordée en choisissant un fichier ne survit pas à une relance ; il faut pour cela un signet à portée de sécurité (security-scoped bookmark), qui est une autre monnaie et n'est pas encore offert. Une application qui stocke le chemin et le lit au lancement suivant est refusée sans explication. Bureau seulement : le sélecteur d'un téléphone répond par une URL à portée et non par un chemin.
Depuis 0.2.0-preview.47
L'autre moitié de IFileDialogs : celui-là demande un chemin à une personne, celui-ci remet un
chemin au système. Ce que fait un scanner de disque après avoir trouvé le coupable, ce que fait une
application avec le rapport qu'elle vient d'écrire et que la personne veut dans son tableur.
Workspace.Reveal(folder); // montrer où il EST, sélectionné, sans l'ouvrir
Workspace.OpenFile(reportPath); // le remettre à ce qui possède ce genre de fichierChacun répond si le système l'a pris. false est une réponse ordinaire et non une exception : un
chemin peut disparaître entre un scan et un clic. La ligne, c'est QUI s'est trompé : le système qui
décline quelque chose qu'on aurait pu lui demander, c'est false ; un chemin relatif, qu'on n'aurait
pu remettre à aucun système, c'est l'erreur de l'appelant et cela lève. Reveal montre un fichier ou
un dossier sélectionné dans l'explorateur de fichiers sans l'ouvrir ; OpenFile prend un FICHIER et
le remet à ce qui possède son genre. Bureau seulement, pour la même raison que les dialogues : un
téléphone a une feuille de partage et un fournisseur de documents, qui ne sont pas cela.
Depuis 0.2.0-preview.49
Un lien est remis au système de la même façon sur un bureau et sur un téléphone : un écran de réglages avec un lien vers sa propre page de confidentialité, un bouton "Contacter le support", l'adresse d'une boutique ouverte dans la carte de la personne.
await Links.OpenUrlAsync(new Uri("https://acme.example/privacy")); // un navigateur
await Links.OpenUrlAsync(new Uri("mailto:support@acme.example")); // l'app de messagerie, une fois que Program.cs ouvre mailto
if (Links.CanOpen(acmeLink)) … // une QUESTION : pas de dialogue, pas d'effet de bordChacun répond si le système l'a pris, et false est une réponse ordinaire : un schéma peut n'avoir
rien derrière lui sur cette machine. Une URL relative, qu'on n'aurait pu remettre à aucun système,
c'est l'erreur de l'appelant et cela lève. Ouvrir est asynchrone parce qu'un iPhone dit s'il a ouvert
le lien dans un callback, plus tard ; un bureau et Android répondent tout de suite, et la tâche est
déjà terminée.
Chaque shell de Photon le réalise. Le navigateur n'en enregistre aucun : là, un lien est une ancre,
et le navigateur l'achemine déjà. C'étaient IWorkspace.OpenUrl et IWorkspace.CanOpen jusqu'à ce
qu'un téléphone en ait besoin, et un contrat qui montre aussi un fichier dans un explorateur de
fichiers est un contrat qu'un téléphone ne pourrait jamais enregistrer.
Depuis 0.2.0-preview.64
Une URL est remise par schéma, et seulement par les schémas que votre application ouvre.
Remettre une URL au système d'exploitation la remet à ce qui revendique son schéma : https atteint
un navigateur, ce que veut dire "ouvrir ce lien" ; file lance ce que le chemin nomme ; d'autres
schémas ouvrent des sessions ou exécutent l'application qui les a enregistrés. Un lien arrivé dans
du contenu (un élément de flux, une valeur renvoyée par un serveur) ne doit jamais pouvoir choisir
parmi ceux-là, donc chaque réalisation consulte une OpenUrlPolicy avant la plateforme. Par défaut
elle remet http, https et chaque schéma auquel l'application a déclaré répondre avec
builder.Bundle.UrlScheme(…) : le SDK les connaît, donc personne ne les dit deux fois. Tout le
reste est une ligne dans Program.cs :
builder.Workspace.OpensMail(); // mailto:, un bouton "Contacter le support"
builder.Workspace.OpensPhone().OpensMessages(); // tel:, sms:
builder.Workspace.Opens("x-apple.systempreferences"); // celui d'une autre application, ou du système, par son nomUne URL refusée répond false, exactement comme le fait le système pour un schéma que rien ne
revendique, et la réalisation journalise le schéma et la seule ligne qui l'ouvrirait. Ce n'est pas
une exception, parce que la politique existe pour les URL que l'application n'a pas écrites, et un
lien hostile doit être quelque chose qu'un gestionnaire de clic peut remettre sans try/catch.
CanOpen applique la même politique, donc un bouton qui en dépend disparaît exactement pour les
liens que OpenUrlAsync refuserait. file ne peut pas être activé : un fichier a des portes typées,
OpenFile et Reveal, qui vérifient que le chemin existe et ne lancent jamais un dossier. Une
application qui a ses propres règles enregistre sa propre OpenUrlPolicy avant Build(), et celle
du builder s'efface.
Depuis 0.2.0-preview.50
Un téléphone ne répond CanOpen que pour les schémas sur lesquels le manifeste de l'application dit
qu'elle peut l'interroger : LSApplicationQueriesSchemes sur iOS, et queries à partir d'Android 11,
où une application qui n'en liste aucun s'entend dire que rien n'ouvre même un lien https. Le build
écrit les deux à partir des mêmes lignes builder.Workspace.Opens… dont la politique est faite (avec
les deux schémas du web sur Android), donc la question dit la vérité et personne n'ouvre un
Info.plist ni un manifeste. C'est aussi pourquoi un schéma ouvert là est une constante : un schéma
construit à l'exécution n'atteint pas un manifeste écrit au moment du build, et le build s'arrête à
cette ligne (EQ3007). Une application qui enregistre sa propre OpenUrlPolicy
voit ses lignes Allowing(…) lues de la même façon.
Ce que chacun ouvre :
-
tel:le composeur du téléphone, avec le numéro dedans. C'est la personne qui passe l'appel. -
sms:l'app de messages, etmailto:l'app de messagerie. - Un lieu, comme lien de carte :
https://maps.apple.com/?q=…ouvre Plans sur un iPhone, etgeo:0,0?q=…(avecOpens("geo")) l'app de cartes sur Android. Un appel qui ouvre un lieu sur tous les systèmes n'existe pas encore.
Le simulateur iOS n'a ni l'app Téléphone ni Mail, donc là tel: et mailto: répondent false.
ILocation, IMotionSensor et INetworkStatus sont celles dont la réponse CHANGE, donc elles
rendent un IDisposable depuis Subscribe plutôt qu'une valeur. Libérez-le quand le composant s'en
va, sinon l'abonnement survit à ce qu'il mettait à jour.
IClock est celle dont la réponse n'est RIEN D'AUTRE que du changement, et c'est ce qui manquait à
un écran qui avance tout seul : un carrousel, un compteur en direct, un statut interrogé à
intervalles réguliers. Tout le reste ici réagit à quelque chose qu'une personne a fait.
public sealed class Carousel(IClock clock) : StatefulComponent
{
private IDisposable? _tick;
private int _slide;
protected override void OnMount() =>
_tick = clock.Every(TimeSpan.FromSeconds(4), () => SetState(() => _slide = (_slide + 1) % 3));
protected override void OnUnmount() => _tick?.Dispose();
}La paire est le contrat : OnMount s'abonne, OnUnmount libère, et sans la seconde moitié chaque
navigation laisse un minuteur tourner contre un composant que personne ne peut voir. Sur un SERVEUR
l'horloge ne bat jamais, ce qui est la bonne réponse et non une réponse manquante : un serveur rend
une seule frame, donc le premier affichage est l'état avec lequel le composant a été construit et
les battements commencent à l'hydratation. Un téléphone qui a passé dix minutes dans une poche
revient à UN tick, jamais à six cents.
C'est une horloge PÉRIODIQUE et non une horloge de frames, délibérément. L'animation par frame a
besoin de deux choses qui n'existent pas encore, la rétention d'état positionnel à travers le
réconciliateur et un canal de géométrie dans StyleChannels, et un callback à 60 Hz sans elles
serait la façon la plus lente d'animer.
INetworkStatus rapporte la joignabilité telle que la plateforme la voit, pas la promesse qu'un
hôte précis répondra. Une application qui affiche un "en ligne" périmé est pire qu'une application
qui ne dit rien.
PermissionState a quatre réponses, et la troisième est celle que les applications oublient :
-
NotDetermined: jamais demandé. Demander affiche l'invite du système. -
Granted. -
Denied: refusé, et redemander ne fait rien. Seuls les réglages du système peuvent le changer, donc un bouton qui redemande est un bouton qui ne fait rien ; envoyez plutôt la personne vers les réglages. - La plateforme peut aussi rapporter une restriction que l'utilisateur ne peut pas lever du tout.
En natif, les textes que l'OS affiche dans l'invite viennent de l'assembly :
[assembly: PhotonCapability("camera", "Scan a barcode to add an item.")]Le build les lit vers le manifeste de la plateforme (Info.plist, le manifeste Android), donc la
raison qu'un utilisateur lit est énoncée une seule fois, dans l'application, à côté du code qui en a
besoin.
Traverse par NOM, jamais par ordinal. Le manifeste compare la chaîne de la capacité. Insérer une valeur au milieu d'un enum a un jour transformé Location en Motion dans un manifeste livré : le build était vert et l'application a demandé la mauvaise autorisation.
Un composant ne demande jamais sur quelle cible il se trouve. Quelqu'un qui choisit des cibles a quand même besoin de savoir ce que chacune remet, donc voici la réponse par contrat, pour les shells natifs :
| Contrat | macOS | iOS | Android | Windows |
|---|---|---|---|---|
IPhotoLibrary |
✓ | ✓ | ✓ | ✓ |
ICamera |
✓ | ✓ | ✓ | indisponible |
ILocation |
✓ | ✓ | ✓ | indisponible |
IBiometrics |
✓ | ✓ | ✓ | indisponible |
IMotionSensor |
✓ | ✓ | ✓ | indisponible |
INetworkStatus |
✓ | ✓ | ✓ | ✓ |
ITextClipboard |
✓ | ✓ | ✓ | ✓ |
IAppStorage |
✓ | ✓ | ✓ | ✓ |
ISecretStore |
✓ | ✓ | ✓ | ✓ |
IDeepLinks |
✓ | ✓ | ✓ | ✓ |
IFileDialogs |
✓ | null |
null |
✓ |
IWorkspace |
✓ | null |
null |
✓ |
IUrlLauncher |
✓ | ✓ | ✓ | ✓ |
-
✓ : le shell a une réalisation. Savoir si CETTE machine a le matériel reste la réponse propre
au contrat :
IMotionSensor.IsAvailablevautfalsesur un Mac, qui a l'API et aucun capteur. -
indisponible : le shell enregistre un objet dont
IsAvailablevaut toujoursfalse, donc une page écrite pour un téléphone se construit quand même là et montre sa branche "pas sur cet appareil". -
null: le shell n'enregistre rien, et le service se résout ànull.IFileDialogsetIWorkspacesont des contrats de bureau, et c'est leur réponse sur un téléphone.
Le navigateur est la section suivante.
ICamera répond sur Android comme sur iOS (Depuis 0.2.0-preview.62). Jusque-là
UseCamera(reason) écrivait l'autorisation dans le manifeste et le shell n'enregistrait rien, donc
un CameraPreview dessinait son état indisponible sur le seul téléphone à qui elle manquait.
public sealed class ScanPage(ICamera? camera) : StatefulComponent
{
private ICameraSession? _session;
private async void StartAsync()
{
if (camera is null) return;
var session = await camera.StartPreviewAsync(); // demande tant que le système le fait encore, null en cas de refus
SetState(() => _session = session);
}
public override VisualNode Build(ComponentContext context) =>
CameraPreview(_session, 320, 320, cornerRadius: new CornerRadii(24), label: "Scanner");
}-
L'autorisation est à l'intérieur de
StartPreviewAsync. Il affiche l'invite du système chaque fois que le système en affiche encore une, et répondnullquand la personne refuse.PermissionlitDeniedseulement quand redemander n'afficherait rien, ce que le mot veut dire sur chaque cible : sur iOS c'est le premier refus, et sur Android 11 et suivants c'est le second, donc après un refus il y lit encoreNotDeterminedet une seconde demande affiche l'invite. Android lui-même dit seulement "accordé" ou "non" : le shell se souvient de la dernière réponse, et c'est ce qui distingue une autorisation jamais demandée d'une autorisation refusée pour de bon. - Les frames arrivent sous forme de pixels, à l'endroit pour la façon dont l'appareil est tenu. L'aperçu est un nœud ordinaire : il rogne, prend un rayon de coin et se place sous ce qui est dessiné par-dessus.
-
Capture()renvoie la frame à l'écran sous forme d'image fixe encodée (image/jpegsur Android). - La caméra se ferme quand l'application quitte l'écran et se rouvre quand elle revient, dans la même session. Une application seulement en pause et encore visible, à côté d'une autre fenêtre ou sous une invite du système, garde son aperçu.
Le navigateur réalise ce qu'il peut honnêtement (les photos par un champ de fichier, la caméra, la localisation et le réseau par leurs API web) et enregistre le reste comme indisponible plutôt qu'absent, donc une page qui prend une capacité reçoit quand même un objet et montre son repli au lieu d'échouer à se construire.
Une page prend ce dont elle a besoin par son constructeur sur chaque cible :
public sealed class ProfilePage(IPhotoLibrary photos) : StatefulComponent
{
public override VisualNode Build(ComponentContext context) =>
photos.IsAvailable ? Button(label: "Choose a picture", onPressed: Pick) : Text("No library here");
}En natif, ActivatorUtilities la résout. Dans le navigateur le constructeur transpilé la résout
lui-même, par le NOM de l'interface : un type C# n'existe pas là à l'exécution, mais IPhotoLibrary
en tant que chaîne si, et les deux côtés s'accordent dessus.
Quelle que soit la forme de constructeur qui la déclare. Un constructeur primaire prenait un chemin
différent dans le parseur jusqu'à 0.2.0-preview.33, et une section écrite
PairLoop(IClock clock) recevait if (clock !== undefined) this.clock = clock à la place :
personne, en la composant au milieu d'un arbre, ne passe une horloge, donc le champ restait
undefined et le composant était inerte, en silence, sur une seule cible.
Personne ne la passe non plus. La dépendance quitte le constructeur émis, donc elle quitte aussi le
SITE D'APPEL et la factory générée : Quark(mood, size) est toute la signature que voit un
appelant, et new Quark(clock, mood, size) en C# laisse tomber l'horloge en sortant plutôt que de
décaler d'un cran chaque argument qui la suit. La règle est demandée au modèle, pas devinée à partir
d'un nom : un paramètre de constructeur dont le type est une INTERFACE est une dépendance, tout le
reste est une donnée que l'appelant passe. Un composant prend ce qu'il dessine (un libellé, une
variante, un callback) et aucun de ceux-là n'est jamais une interface. (IReadOnlyList<T> et
IEnumerable<T> sont des données, explicitement.)
Depuis 0.2.0-preview.18
public sealed class CopyButton : StatelessComponent
{
public override VisualNode Build(ComponentContext context) =>
context.GetService<ITextClipboard>() is { } clipboard
? IconButton(Icons.Copy, onPressed: () => clipboard.Write(_code))
: new Box(); // pas de presse-papiers ici, donc ne rien dessiner plutôt qu'un bouton mort
}L'injection par constructeur reste la meilleure réponse là où elle convient : explicite, testable,
lisible dans la signature. Mais elle n'atteint que la PAGE. Tout ce qui est en dessous devait
recevoir la même chose à la main : une carte avec un bouton Copier a besoin d'un ITextClipboard,
donc l'article au-dessus en portait un qu'il n'utilisait jamais, et la section au-dessus de lui le
portait aussi. Un composant qui gagnait un besoin forçait une modification dans chaque ancêtre entre
lui et la page.
context.GetService<T>() répond null quand la cible n'a pas la capacité, ce qui est la réponse que
l'appelant de toute capacité doit gérer de toute façon.
D'OÙ elle se résout est l'affaire de l'hôte : le SSR utilise le conteneur de la REQUÊTE (donc un enregistrement scoped fonctionne et les enregistrements propres à une page gagnent), le navigateur utilise ce que le boot a enregistré, une application Photon utilise celui du shell. Le composant pose la même question partout.
Sur le WEB, cela n'est arrivé en état de marche qu'en 0.2.0-preview.21. L'argument de type était perdu à la transpilation : la stratégie qui transforme l'appel en clé reconnaissait l'ancien
RenderContextde Core et unIServiceProvider, etComponentContextn'est ni l'un ni l'autre, donc l'appel retombait sur le chemin d'invocation ordinaire, qui laisse tomber les arguments de type. Chaque page demandait une capacité sans aucun nom et recevait null, sur la seule cible que la fonctionnalité existe pour servir.
Depuis 0.2.0-preview.33
public sealed class PairLoop : StatefulComponent
{
private IDisposable? _tick;
protected override void OnMount() =>
_tick = GetService<IClock>()?.Every(Beat, () => SetState(() => _step++));
protected override void OnUnmount() => _tick?.Dispose();
}OnMount est l'endroit où un abonnement a sa place. Il s'exécute une fois, sur l'instance que le
réconciliateur garde, et il fait la paire avec OnUnmount, ce qui empêche une page qu'on a quittée
de battre pour toujours. Il ne reçoit aucun ComponentContext, donc avant cela la seule façon
d'atteindre une capacité en profondeur était à l'intérieur de Build derrière un drapeau
d'exécution unique : l'abonnement vivait dans la seule méthode que le framework appelle à
répétition, et l'appariement avec OnUnmount se lisait comme un accident.
L'accesseur sur le composant répond exactement ce que répond context.GetService<T>(), depuis
n'importe où dans le composant : un hook de cycle de vie, un gestionnaire d'événement, Build
lui-même.
Depuis 0.2.0-preview.33
Le C# le dit déjà, donc le framework le lit plutôt que de redemander :
| Écrit | Sur une cible qui l'a | Sur une cible qui ne l'a pas |
|---|---|---|
IClock clock |
la capacité | une erreur qui nomme la capacité et le composant |
IClock? clock |
la capacité |
null, que le composant gère |
Un paramètre non nullable est le composant qui promet qu'il ne peut pas fonctionner sans lui, donc
un null qu'on lui remet échoue plus tard, dans du code qui ne mentionne jamais les capacités, sur
la seule cible où l'écran est cassé. Le message nomme quelle capacité, quel composant et les deux
issues : l'enregistrer auprès de l'hôte, ou déclarer le paramètre nullable. Les deux cibles le
disent, sinon le navigateur serait l'indulgent et le bug n'existerait que là.
Un fichier où nullable est désactivé ne donne aucun signal dans un sens ou dans l'autre, et garde le sens qu'il a toujours eu.
Depuis 0.2.0-preview.33
using var _ = CapabilityScope.With<IClock>(fake);
var section = new PairLoop();
section.NotifyMounted();Une capacité, par-dessus ce qui est déjà en vigueur, remise en place à la libération. L'imbrication
se compose. La cérémonie que cela remplace était un résolveur écrit à la main, qui répondait null
à toute AUTRE capacité que le composant pouvait demander et devait être défait dans un finally
qu'un test pouvait oublier.
Depuis 0.2.0-preview.13
La même page doit pouvoir être rendue sur le serveur, et le serveur n'a pas de caméra. Chaque capacité s'y résout à une réalisation ABSENTE : elle se déclare indisponible et ne rend rien.
Sans elle la page ne pourrait pas être construite du tout : aucun constructeur que le conteneur puisse satisfaire, et la requête finissait en 500. La seule page qui fait quelque chose était la seule page qu'un robot d'indexation ne voyait jamais, et la visiteuse attendait le JavaScript juste pour apprendre que la page existe.
Absente, délibérément, et non simulée. Il n'y a pas de caméra dans un centre de données et le
localStorage de la visiteuse est sur la machine de la visiteuse, donc un faux côté serveur serait
pire que l'échec : la page rendrait une chose, le navigateur en hydraterait une autre, et l'écart
serait imputé au réconciliateur. La page prend la branche de disponibilité qu'elle doit déjà avoir,
et la vraie capacité du boot client remplace le repli au premier rendu du client.
INetworkStatus est l'exception, et l'exception est tout l'intérêt : rapporter hors ligne
incrusterait la bannière hors ligne dans le balisage que voient chaque robot d'indexation et chaque
premier affichage, pour des visiteurs qui viennent de prouver qu'ils sont en ligne en récupérant la
page. Elle répond en ligne.
Enregistrée avec
TryAdd, donc une application qui a une vraie réponse côté serveur (un stockage adossé à la session de l'utilisateur) enregistre la sienne et gagne.
🏁 Commencer ici
🏗️ Architecture
- Vue d'ensemble de l'architecture
- Composants write-once
- Surface déclarative
- Architecture des paquets
- Composants
- Styles
- Localisation
- Analytics et GTM
📱 Write-once
- Moteur Photon
- Design System
- Capacités
- Stockage
- Formulaires
- Éditeur de code
- Markdown
- Mermaid
- Rendu des e-mails
⚙️ Compilation
- Compilateur
- Fonctionnalités C# prises en charge
- Résolution des types externes
- Flux de build
- Diagnostics
⚡ Runtime
🔌 Serveur
🎨 Écosystème
- Image · Graphiques · Icônes · Lottie
🚀 Développement