Skip to content

Capabilities fr

Edgar Mesquita edited this page Oct 14, 2026 · 5 revisions

Capacités de l'appareil

🌐 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

Le thread depuis lequel on peut toucher à une UI

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

L'horloge des frames

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

Un lien vers l'intérieur de l'application

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à Launch est déjà renseigné quand le premier arbre est construit, et il est exact : une application ouverte depuis son icône répond null, 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 https qui 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

Demander un fichier à une personne

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

Remettre un fichier au système

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 fichier

Chacun 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

Ouvrir un lien

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 bord

Chacun 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

Quels liens il ouvre

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 nom

Une 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

Sur un téléphone

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, et mailto: l'app de messagerie.
  • Un lieu, comme lien de carte : https://maps.apple.com/?q=… ouvre Plans sur un iPhone, et geo:0,0?q=… (avec Opens("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.

Des réponses qui ne tiennent pas en place

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.

Autorisations

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.

Quel shell répond

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.IsAvailable vaut false sur un Mac, qui a l'API et aucun capteur.
  • indisponible : le shell enregistre un objet dont IsAvailable vaut toujours false, 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. IFileDialogs et IWorkspace sont des contrats de bureau, et c'est leur réponse sur un téléphone.

Le navigateur est la section suivante.

La caméra sur un téléphone

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épond null quand la personne refuse. Permission lit Denied seulement 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 encore NotDetermined et 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/jpeg sur 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 web

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.)

Un composant au milieu d'un arbre

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 RenderContext de Core et un IServiceProvider, et ComponentContext n'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.

Là où il n'y a pas de contexte

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.

Ce que fait une capacité manquante

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.

En remettre une dans un test

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.

Le rendu sur le serveur

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.

Clone this wiki locally