Repository navigation
Capabilities de
🌐 Diese Seite auf: English · Português · Español · Français · Deutsch
Was die Maschine kann (Kamera, Standort, Fotos, Biometrie, das Netzwerk) als Dienste, die per Konstruktor genommen werden, je Host realisiert. Eine Komponente fragt nach einer Fähigkeit und erfährt nie, welches Ziel geantwortet hat.
public sealed class ScanShell(ICamera? camera, ILocation? location) : StatefulComponent
{
private async void LocateAsync()
{
if (location is null) return; // dieser Host hat keinen
var here = await location.GetCurrentAsync();
}
}Nullable, immer. Ein Host, der etwas nicht kann, registriert nichts, der Dienst löst also zu
null auf, und die Anwendung zeigt, was sie ohnehin schon zu zeigen weiß. Das ist überall die
Antwort des Frameworks: das Fehlen melden, statt mit einem Stub so zu tun, der später scheitert, in
einem schlechteren Moment.
| Fähigkeit | Was sie beantwortet | Seit |
|---|---|---|
IPhotoLibrary |
GetPermissionAsync(), PickImageAsync(), ein Bild, das die Person gewählt hat |
0.2.0-preview.1 |
ICamera |
StartPreviewAsync() für eine ICameraSession: eine Live-Textur, und deren Capture() für ein Standbild |
0.2.0-preview.1 |
ILocation |
GetCurrentAsync(), und Subscribe(…) für den Strom der Änderungen |
0.2.0-preview.1 |
IBiometrics |
IsAvailable, AuthenticateAsync(reason): Face ID / Touch ID / das Eigene der Plattform |
0.2.0-preview.1 |
IMotionSensor |
Subscribe(…) für Bewegungsmesswerte des Geräts |
0.2.0-preview.1 |
INetworkStatus |
Current, und Subscribe(…): Erreichbarkeit und was sie trägt |
0.2.0-preview.1 |
ITextClipboard |
Read() / Write(text), für den eigenen Kopieren-Button einer Seite und das, worüber ein natives Feld kopiert, ausschneidet und einfügt. Jedes Ziel, die Telefone eingeschlossen |
0.2.0-preview.1 |
IThemeController |
Der Hell/Dunkel-Schalter. Siehe ServerIntegration | 0.2.0-preview.1 |
IAppStorage / ISecretStore
|
Dauerhafte Einstellungen und Geheimnisse. Siehe Speicher | 0.2.0-preview.10 |
IClock |
Every(interval, onTick): die Zeit selbst, als etwas, das eine Komponente abonnieren kann |
0.2.0-preview.31 |
IFrameTicker |
OnFrame(tick): ein Callback vor jedem Frame, mit dessen Zeit und dem Delta, für das, was ein Stil nicht ausdrücken kann |
0.2.0-preview.46 |
IUiDispatcher |
IsOnUiThread, Post(work): der Thread, von dem aus eine UI berührt werden darf, und der Weg dorthin |
0.2.0-preview.46 |
IConsent |
State, Grant(), Deny(): die Antwort der Besucherin auf nicht notwendiges Tracking, der Cookie, von dem der GTM-Installer abhängt. Siehe Analytics
|
0.2.0-preview.46 |
IDeepLinks |
Launch, Subscribe(onOpened): die URLs, MIT denen diese Anwendung geöffnet wird. Photon |
0.2.0-preview.47 |
IFileDialogs |
PickFile, PickFiles, PickFolder, PickSavePath: eine PERSON nach einer Datei fragen. Desktop |
0.2.0-preview.47 |
IWorkspace |
Reveal(path), OpenFile(path): eine DATEI dem System übergeben. Desktop |
0.2.0-preview.49 |
IUrlLauncher |
OpenUrlAsync(url), CanOpen(url): einen LINK dem System übergeben, nach den Schemata, die die Anwendung öffnet. Photon, die Telefone eingeschlossen |
0.2.0-preview.64 |
IServerEvents |
Subscribe(topic, onEvent, onRefused), Connection, OnConnectionChanged(…): was der Server auf einem typisierten Topic veröffentlicht, über eine Verbindung je Seite. Web. Siehe Server-Events
|
0.2.0-preview.61 |
Seit 0.2.0-preview.31
Ein Bildschirm wird auf EINEM Thread gebaut, ausgelegt und gezeichnet: dem Haupt-Thread der
Plattform auf jedem nativen Ziel, und dem einzigen Thread, den es in einem Browser gibt. Alles
andere, was eine Anwendung tut, passiert anderswo: ein Scanner, der eine Platte durchläuft, ein
Download, ein Lesevorgang in einer Datenbank. Wenn eines davon fertig ist, will es zeigen, was es
gefunden hat, und SetState ist der Weg.
Das war früher ein Data Race ohne Fehlermeldung. SetState änderte die Felder der Komponente von
dem Thread aus, der es gerade aufrief, während der Render-Thread genau diesen Baum las: ein Frame,
gezeichnet aus halb altem Zustand, eine Sammlung, die aufgezählt wird, während sie wächst, ein
Absturz, der sich einmal pro Woche reproduziert.
Nichts, was einzuschalten wäre. SetState fragt zuerst IUiDispatcher. Abseits des UI-Threads
wird die ganze Änderung eingereiht und läuft am Anfang des nächsten Frames, bevor irgendetwas gebaut
wird; auf ihm läuft sie inline, genau wie zuvor. Wo es keinen anderen Thread gibt, gibt es nichts zu
marshallen, und die ehrliche Antwort ist, dass die Fähigkeit sich als bereits auf dem UI-Thread
meldet: Der Browser hat einen Thread, und ein Server rendert eine Anfrage auf dem Thread, der
gefragt hat.
Und die Linie, die er nicht überschreitet. Der Dispatcher macht den Zustand sicher, der durch
SetState reist. Geteilter Zustand, den ein Worker-Thread AUSSERHALB davon berührt, bleibt Sache
der Anwendung: ein Modell, das die Anwendung auf einem Hintergrund-Thread stutzt, eine Sammlung, an
die ein Task anhängt. Eine Anwendung, die diese Fähigkeit als "Threading ist jetzt erledigt" läse
und ihre eigene Absicherung löschte, würde das Einzige löschen, was zwischen dem Render-Durchgang
und einem Baum steht, der unter ihm verändert wird. Was der Dispatcher dieser Absicherung gibt, ist
eine Garantie, die sich lohnt: Das SetState, das ein Flag SETZT, läuft inline, wenn es vom
UI-Thread kommt, das Flag ist also schon oben, bevor die Arbeit beginnt.
Seit 0.2.0-preview.46
IClock ist bewusst periodisch (Hunderte von Millisekunden und mehr), und ihr eigener Zaun sagt,
warum Animation pro Frame darüber nicht angeboten wurde. IFrameTicker ist dieser andere Kontrakt:
ein Callback vor jedem Frame, der die Zeit des Frames und das Delta seit dem letzten des Abonnenten
trägt, auf dem UI-Thread und bevor der Baum gebaut wird, sodass ein SetState in einem Tick das
ist, was genau dieser Frame zeichnet.
protected override void OnMount() =>
_ticking = Ticker.OnFrame(tick => SetState(() => _phase += tick.DeltaMs / 1000f));
protected override void OnUnmount() => _ticking?.Dispose();Ein Abonnement HÄLT DIE FRAMES AM LAUFEN, was der Zweck ist und auch der Preis: Geben Sie es frei, wenn die Bewegung aufhört. Auf einem Server gibt es keine Frames, das Abonnieren gelingt also und feuert nie.
Die meiste Bewegung braucht nichts davon. Eine Stiländerung gleitet inzwischen von selbst (siehe Photon); der Ticker ist für das, was ein Stil nicht ausdrücken kann: Physik, eine Simulation, Geometrie, die in jedem Frame aus Daten neu berechnet wird, die nie stillstehen.
Seit 0.2.0-preview.46
Das acme://activate?key=… einer Lizenz-E-Mail, ein Link zurück von einer Anmeldung im Browser,
eine Kollegin, die einen Link auf einen Bildschirm einfügt. Das Schema zu deklarieren ist
builder.Bundle.UrlScheme("acme") (siehe
Photon); IDeepLinks ist die Anwendung,
die LIEST, womit sie geöffnet wurde. Ohne beide Hälften öffnet ein Deep Link die Anwendung an ihrer
Haustür und lässt fallen, was er trug, was für die Person, die geklickt hat, genau wie ein kaputter
Link aussieht.
protected override void OnMount()
{
if (Links.Launch is { } opened) Activate(opened); // was diese Anwendung GESTARTET hat
_link = Links.Subscribe(Activate); // und jeder von jetzt an
}
protected override void OnUnmount() => _link?.Dispose();Tun Sie beides, und hier ist der Grund. Launch ist auf macOS in einem Konstruktor nicht
lesbar, gemessen statt angenommen. Die Start-URL kommt als AppleEvent an, AppleEvents werden VON der
Run Loop zugestellt, und die Run Loop startet, nachdem der erste Baum gebaut ist, ein Kaltstart
erreicht also einen ABONNENTEN und findet die Eigenschaft einen Moment früher noch null. Das
Abonnement ist, was einen Kaltstart auffängt; die Eigenschaft ist dafür da, später zu fragen, auf
einem Bildschirm, der danach gebaut wurde. Eine Anwendung, die beides tut, liegt überall richtig.
Launch wird einem Abonnenten nicht erneut zugestellt, eine Anwendung, die beides tut, handelt also
einmal. Eine URL, die die Anwendung nicht ERKENNT, wird trotzdem zugestellt: Zu entscheiden, was ein
unbekannter Host bedeutet, ist Sache der Anwendung, und sie zu verschlucken hieße, an ihrer Stelle
zu entscheiden und keine Spur zu hinterlassen. Ein String, der keine URL ist, wird verworfen.
Auf einem Telefon (Seit 0.2.0-preview.63) funktionieren dieselben zwei Zeilen, und dasselbe
builder.Bundle.UrlScheme("acme") ist die ganze Deklaration: Der Build schreibt den URL-Typ, den
ein iPhone liest, und den Link-Filter, den Android abgleicht, und niemand öffnet eine Info.plist
oder ein Manifest.
- Ein Telefon übergibt den Link MIT dem Start, dort ist
Launchalso schon gesetzt, wenn der erste Baum gebaut wird, und es ist exakt: Eine über ihr Icon geöffnete Anwendung antwortetnull, und ein Link, der eine Stunde später angetippt wird, wird von einem Abonnenten gehört und wird nicht zu diesem Wert. - Ein Link, der geöffnet wird, während die Anwendung läuft, erreicht die laufende Anwendung. Auf Android ist eine Anwendung, die ein Schema deklariert, ein einzelner Task, das System übergibt ihr den Link also, statt eine zweite Kopie darüber zu starten, und nur ein Link auf einem Schema, das die Anwendung deklariert hat, wird zugestellt, wer immer ihn geschickt hat.
- Das sind die eigenen Schemata der Anwendung. Ein
https-Link, der die Anwendung ohne Rückfrage öffnet (Universal Links, App Links), braucht auch das Wort der Website und ist noch nicht da.
Nur Photon: Im Web ist ein Deep Link die Adressleiste, und die gehört schon dem Router.
Seit 0.2.0-preview.47
Der eine Weg, auf dem eine Anwendung in der Sandbox irgendetwas berührt, das ihr nicht gegeben
wurde, und der Grund, warum es PhotonEntitlements.UserSelectedFiles gibt.
var folder = await Dialogs.PickFolder("Choose a folder to scan");
var report = await Dialogs.PickSavePath("report.csv",
filters: [new FileFilter("Spreadsheets", "csv")]);Null bedeutet, die Person hat ABGEBROCHEN, was eine gewöhnliche Antwort ist und keine Ausnahme: Wer
einen Dialog schließt, ist an nichts gescheitert, und eine Anwendung, die fangen muss, um das
herauszufinden, ist eine Anwendung, die es vergessen wird. PickFiles antwortet aus demselben Grund
mit einer leeren Liste: Ein Aufrufer, der gleich eine Schleife beginnt, sollte nicht zweimal fragen
müssen.
Rufen Sie von überall auf. Die Panels sind modal und nur auf dem Haupt-Thread erlaubt, und die
Realisierung marshallt selbst über IUiDispatcher, denn ein Panel, das von einem Worker-Thread
geöffnet wird, öffnet sich gar nicht: Es erscheint einfach nie, und nichts wird protokolliert.
Ein Pfad gilt für DIESEN LAUF. In einer Anwendung in der Sandbox überlebt die Erlaubnis, die eine Person durch die Wahl einer Datei erteilt hat, keinen Neustart; dafür braucht es ein Security-Scoped Bookmark, was eine andere Währung ist und noch nicht angeboten wird. Eine Anwendung, die den Pfad speichert und ihn beim nächsten Start liest, wird ohne Erklärung abgewiesen. Nur Desktop: Der Picker eines Telefons antwortet mit einer Scoped URL und nicht mit einem Pfad.
Seit 0.2.0-preview.47
Die andere Hälfte von IFileDialogs: Jener fragt eine Person nach einem Pfad, dieser gibt einen
Pfad an das System zurück. Was ein Festplatten-Scanner tut, nachdem er den Übeltäter gefunden hat,
was eine Anwendung mit dem Bericht tut, den sie gerade geschrieben hat und den die Person in ihrer
Tabellenkalkulation haben will.
Workspace.Reveal(folder); // zeigen, wo es IST, ausgewählt, nicht geöffnet
Workspace.OpenFile(reportPath); // dem übergeben, dem diese Art Datei gehörtJede Methode antwortet, ob das System es angenommen hat. false ist eine gewöhnliche Antwort und
keine Ausnahme: Ein Pfad kann zwischen einem Scan und einem Klick verschwinden. Die Linie ist, WER
falsch lag: Dass das System etwas ablehnt, wonach man hätte fragen können, ist false; ein
relativer Pfad, den man keinem System hätte übergeben können, ist der Fehler des Aufrufers und
wirft. Reveal zeigt eine Datei oder einen Ordner ausgewählt im Dateibrowser, ohne sie zu öffnen;
OpenFile nimmt eine DATEI und übergibt sie dem, dem ihre Art gehört. Nur Desktop, aus demselben
Grund wie die Dialoge: Ein Telefon hat ein Share Sheet und einen Document Provider, und das sind
nicht diese.
Seit 0.2.0-preview.49
Ein Link wird dem System auf einem Desktop und auf einem Telefon gleich übergeben: ein Einstellungsbildschirm mit einem Link auf seine eigene Datenschutzseite, ein Button "Support kontaktieren", die Adresse eines Geschäfts, geöffnet in der Karte der Person.
await Links.OpenUrlAsync(new Uri("https://acme.example/privacy")); // ein Browser
await Links.OpenUrlAsync(new Uri("mailto:support@acme.example")); // die Mail-App, sobald Program.cs mailto öffnet
if (Links.CanOpen(acmeLink)) … // eine FRAGE: kein Dialog, kein NebeneffektJede Methode antwortet, ob das System es angenommen hat, und false ist eine gewöhnliche Antwort:
Hinter einem Schema kann auf dieser Maschine nichts stehen. Eine relative URL, die man keinem System
hätte übergeben können, ist der Fehler des Aufrufers und wirft. Das Öffnen ist asynchron, weil ein
iPhone erst später in einem Callback sagt, ob es den Link geöffnet hat; ein Desktop und Android
antworten sofort, und der Task ist schon erledigt.
Jede Photon-Shell realisiert ihn. Der Browser registriert keinen: Dort ist ein Link ein Anker, und
der Browser leitet ihn bereits weiter. Es waren IWorkspace.OpenUrl und IWorkspace.CanOpen, bis
ein Telefon sie brauchte, und einen Kontrakt, der auch eine Datei im Dateibrowser zeigt, könnte ein
Telefon nie registrieren.
Seit 0.2.0-preview.64
Eine URL wird nach Schema übergeben, und nur nach den Schemata, die Ihre Anwendung öffnet. Eine
URL dem Betriebssystem zu übergeben übergibt sie dem, der ihr Schema beansprucht: https erreicht
einen Browser, was "diesen Link öffnen" bedeutet; file startet, was der Pfad benennt; andere
Schemata öffnen Sitzungen oder führen die Anwendung aus, die sie registriert hat. Ein Link, der in
Inhalten ankam (ein Feed-Eintrag, ein Wert, mit dem ein Server geantwortet hat), darf unter diesen
nie wählen, jede Realisierung befragt also eine OpenUrlPolicy vor der Plattform.
Standardmäßig übergibt sie http, https und jedes Schema, von dem die Anwendung mit
builder.Bundle.UrlScheme(…) deklariert hat, dass sie darauf antwortet: Das SDK kennt diese,
niemand sagt sie also zweimal. Alles andere ist eine Zeile in Program.cs:
builder.Workspace.OpensMail(); // mailto:, ein Button "Support kontaktieren"
builder.Workspace.OpensPhone().OpensMessages(); // tel:, sms:
builder.Workspace.Opens("x-apple.systempreferences"); // das einer anderen Anwendung oder des Systems, nach NamenEine abgelehnte URL antwortet false, genau wie das System bei einem Schema, das niemand
beansprucht, und die Realisierung protokolliert das Schema und die eine Zeile, die es öffnen würde.
Keine Ausnahme, denn die Policy existiert für URLs, die die Anwendung nicht geschrieben hat, und ein
feindseliger Link muss etwas sein, das ein Klick-Handler ohne try/catch übergeben kann. CanOpen
wendet dieselbe Policy an, ein davon abhängiger Button verschwindet also genau für die Links, die
OpenUrlAsync ablehnen würde. file lässt sich nicht freischalten: Eine Datei hat typisierte Türen,
OpenFile und Reveal, die prüfen, dass der Pfad existiert, und nie einen Ordner starten. Eine
Anwendung mit eigenen Regeln registriert ihre eigene OpenUrlPolicy vor Build(), und die des
Builders tritt zurück.
Seit 0.2.0-preview.50
Ein Telefon beantwortet CanOpen nur für die Schemata, nach denen die Anwendung laut ihrem Manifest
fragen darf: LSApplicationQueriesSchemes auf iOS und queries ab Android 11, wo eine Anwendung,
die nichts auflistet, zu hören bekommt, dass nichts auch nur einen https-Link öffnet. Der Build
schreibt beide aus denselben Zeilen builder.Workspace.Opens…, aus denen die Policy gebaut wird
(auf Android mit den beiden Schemata des Webs), also sagt die Frage die Wahrheit, und niemand öffnet
eine Info.plist oder ein Manifest. Deshalb ist ein dort geöffnetes Schema auch eine Konstante: Eines,
das zur Laufzeit gebaut wird, erreicht kein Manifest, das beim Build geschrieben wird, und der Build
hält an dieser Zeile an (EQ3007). Bei einer Anwendung, die ihre eigene
OpenUrlPolicy registriert, werden deren Allowing(…)-Zeilen genauso gelesen.
Was jedes davon öffnet:
-
tel:die Telefon-App mit der Nummer darin. Den Anruf tätigt die Person. -
sms:die Nachrichten-App, undmailto:die Mail-App. - Einen Ort, als Kartenlink:
https://maps.apple.com/?q=…öffnet Karten auf einem iPhone, undgeo:0,0?q=…(mitOpens("geo")) die Karten-App auf Android. Ein Aufruf, der einen Ort auf jedem System öffnet, gibt es noch nicht.
Der iOS-Simulator hat weder die Telefon-App noch Mail, deshalb antworten tel: und mailto: dort
false.
ILocation, IMotionSensor und INetworkStatus sind die, deren Antwort sich ÄNDERT, sie geben aus
Subscribe also ein IDisposable zurück statt eines Werts. Geben Sie es frei, wenn die Komponente
verschwindet, sonst überlebt das Abonnement das, was es aktualisiert hat.
IClock ist die, deren Antwort NICHTS ALS Änderung ist, und sie ist, was einem Bildschirm fehlte,
der sich selbst weiterbewegt: ein Karussell, ein Live-Zähler, ein Status, der pollt. Alles andere
hier reagiert auf etwas, das eine Person getan hat.
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();
}Das Paar ist der Kontrakt: OnMount abonniert, OnUnmount gibt frei, und ohne die zweite Hälfte
lässt jede Navigation einen Timer gegen eine Komponente laufen, die niemand sehen kann. Auf einem
SERVER tickt die Uhr nie, was die richtige Antwort ist und keine fehlende: Ein Server rendert einen
Frame, das erste Bild ist also der Zustand, mit dem die Komponente gebaut wurde, und das Ticken
beginnt bei der Hydration. Ein Telefon, das zehn Minuten in einer Tasche verbracht hat, kommt zu
EINEM Tick zurück, nie zu sechshundert.
Sie ist bewusst eine PERIODISCHE Uhr und keine Frame-Uhr. Animation pro Frame braucht zwei Dinge,
die es noch nicht gibt, die Erhaltung von positionsgebundenem Zustand durch den Reconciler und einen
Geometriekanal in StyleChannels, und ein 60-Hz-Callback ohne sie wäre der langsamste Weg zu
animieren.
INetworkStatus meldet Erreichbarkeit, wie die Plattform sie sieht, kein Versprechen, dass ein
bestimmter Host antworten wird. Eine Anwendung, die ein veraltetes "online" zeigt, ist schlechter
als eine, die nichts sagt.
PermissionState hat vier Antworten, und die dritte ist die, die Anwendungen vergessen:
-
NotDetermined: nie gefragt. Fragen zeigt die Abfrage des Systems. -
Granted. -
Denied: abgelehnt, und erneut zu fragen bewirkt nichts. Nur die Einstellungen des Systems können es ändern, ein Button, der erneut fragt, ist also ein Button, der nichts tut; schicken Sie die Person stattdessen in die Einstellungen. - Die Plattform kann auch eine Einschränkung melden, die der Nutzer gar nicht aufheben kann.
Auf Native kommen die Texte, die das Betriebssystem bei der Abfrage zeigt, aus der Assembly:
[assembly: PhotonCapability("camera", "Scan a barcode to add an item.")]Der Build liest sie in das Manifest der Plattform (Info.plist, das Android-Manifest), der Grund,
den ein Nutzer liest, steht also einmal da, in der Anwendung, neben dem Code, der ihn braucht.
Wandert per NAME hinüber, nie per Ordinalzahl. Das Manifest gleicht den String der Fähigkeit ab. Das Einfügen eines Werts in die Mitte eines Enums machte einmal in einem ausgelieferten Manifest aus Location Motion: Der Build war grün, und die Anwendung fragte nach der falschen Berechtigung.
Eine Komponente fragt nie, auf welchem Ziel sie ist. Wer Ziele auswählt, muss trotzdem wissen, was jedes übergibt, dies ist also die Antwort je Kontrakt, für die nativen Shells:
| Kontrakt | macOS | iOS | Android | Windows |
|---|---|---|---|---|
IPhotoLibrary |
✓ | ✓ | ✓ | ✓ |
ICamera |
✓ | ✓ | ✓ | nicht verfügbar |
ILocation |
✓ | ✓ | ✓ | nicht verfügbar |
IBiometrics |
✓ | ✓ | ✓ | nicht verfügbar |
IMotionSensor |
✓ | ✓ | ✓ | nicht verfügbar |
INetworkStatus |
✓ | ✓ | ✓ | ✓ |
ITextClipboard |
✓ | ✓ | ✓ | ✓ |
IAppStorage |
✓ | ✓ | ✓ | ✓ |
ISecretStore |
✓ | ✓ | ✓ | ✓ |
IDeepLinks |
✓ | ✓ | ✓ | ✓ |
IFileDialogs |
✓ | null |
null |
✓ |
IWorkspace |
✓ | null |
null |
✓ |
IUrlLauncher |
✓ | ✓ | ✓ | ✓ |
-
✓: Die Shell hat eine Realisierung. Ob DIESE Maschine die Hardware hat, bleibt die eigene
Antwort des Kontrakts:
IMotionSensor.IsAvailableistfalseauf einem Mac, der die API hat und keinen Sensor. -
nicht verfügbar: Die Shell registriert ein Objekt, dessen
IsAvailableimmerfalseist, eine für ein Telefon geschriebene Seite wird dort also trotzdem konstruiert und zeigt ihren Zweig "nicht auf diesem Gerät". -
null: Die Shell registriert nichts, und der Dienst löst zunullauf.IFileDialogsundIWorkspacesind Desktop-Kontrakte, und das ist ihre Antwort auf einem Telefon.
Der Browser ist der nächste Abschnitt.
ICamera antwortet auf Android wie auf iOS (Seit 0.2.0-preview.62). Bis dahin schrieb
UseCamera(reason) die Berechtigung ins Manifest, und die Shell registrierte nichts, eine
CameraPreview zeichnete ihren Nicht-verfügbar-Zustand also auf dem einen Telefon, dem sie fehlte.
public sealed class ScanPage(ICamera? camera) : StatefulComponent
{
private ICameraSession? _session;
private async void StartAsync()
{
if (camera is null) return;
var session = await camera.StartPreviewAsync(); // fragt, solange das System noch fragt, null bei Ablehnung
SetState(() => _session = session);
}
public override VisualNode Build(ComponentContext context) =>
CameraPreview(_session, 320, 320, cornerRadius: new CornerRadii(24), label: "Scanner");
}-
Die Berechtigung steckt in
StartPreviewAsync. Es zeigt die Abfrage des Systems, wann immer das System noch eine zeigt, und antwortetnull, wenn die Person ablehnt.PermissionliestDeniednur, wenn erneutes Fragen nichts zeigen würde, was das Wort auf jedem Ziel bedeutet: Auf iOS ist das die erste Ablehnung, und auf Android 11 und neuer die zweite, nach einer Ablehnung liest es dort also nochNotDetermined, und ein zweites Fragen zeigt die Abfrage. Android selbst sagt nur "erteilt" oder "nicht": Die Shell merkt sich die letzte Antwort, und die unterscheidet eine nie erfragte Berechtigung von einer endgültig abgelehnten. - Frames kommen als Pixel an, aufrecht für die Art, wie das Gerät gehalten wird. Die Vorschau ist ein gewöhnlicher Knoten: Sie beschneidet, nimmt einen Eckenradius und liegt unter allem, was über sie gezeichnet wird.
-
Capture()gibt den Frame auf dem Bildschirm als kodiertes Standbild zurück (image/jpegauf Android). - Die Kamera schließt, wenn die Anwendung den Bildschirm verlässt, und öffnet wieder, wenn sie zurückkommt, in derselben Sitzung. Eine Anwendung, die nur pausiert und noch sichtbar ist, neben einem anderen Fenster oder unter einer Abfrage des Systems, behält ihre Vorschau.
Der Browser realisiert, was er ehrlich kann (Fotos über ein Datei-Eingabefeld, Kamera, Standort und Netzwerk über ihre Web-APIs), und registriert den Rest als nicht verfügbar statt abwesend, eine Seite, die eine Fähigkeit nimmt, erhält also trotzdem ein Objekt und zeigt ihren Fallback, statt an der Konstruktion zu scheitern.
Eine Seite nimmt auf jedem Ziel, was sie braucht, über ihren Konstruktor:
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");
}Nativ löst ActivatorUtilities sie auf. Im Browser löst der transpilierte Konstruktor sie selbst
auf, über den NAMEN der Schnittstelle: Ein C#-Typ existiert dort zur Laufzeit nicht, aber
IPhotoLibrary als String schon, und beide Seiten sind sich darüber einig.
Welche Konstruktorform sie auch deklariert. Ein primärer Konstruktor nahm bis 0.2.0-preview.33
einen anderen Weg durch den Parser, und ein Abschnitt, der als PairLoop(IClock clock) geschrieben
war, bekam stattdessen if (clock !== undefined) this.clock = clock: Niemand, der ihn mitten in
einem Baum zusammensetzt, übergibt eine Uhr, das Feld blieb also undefined, und die Komponente war
träge, still, auf nur einem Ziel.
Es übergibt sie auch niemand. Die Abhängigkeit verlässt den emittierten Konstruktor, sie verlässt
also auch die AUFRUFSTELLE und die generierte Factory: Quark(mood, size) ist die ganze Signatur,
die ein Aufrufer sieht, und new Quark(clock, mood, size) in C# lässt die Uhr auf dem Weg hinaus
fallen, statt jedes Argument danach um eine Stelle zu verschieben. Die Regel wird das Modell
gefragt, nicht aus einem Namen geraten: Ein Konstruktorparameter, dessen Typ eine SCHNITTSTELLE ist,
ist eine Abhängigkeit, alles andere sind Daten, die der Aufrufer übergibt. Eine Komponente nimmt,
was sie zeichnet (ein Label, eine Variante, einen Callback), und nichts davon ist je eine
Schnittstelle. (IReadOnlyList<T> und IEnumerable<T> sind Daten, ausdrücklich.)
Seit 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(); // keine Zwischenablage hier, also nichts zeichnen statt eines toten Buttons
}Konstruktor-Injektion bleibt die bessere Antwort, wo sie passt: explizit, testbar, in der Signatur
lesbar. Aber sie reicht nur bis zur SEITE. Alles darunter musste dasselbe von Hand gereicht
bekommen: Eine Karte mit einem Kopieren-Button braucht ein ITextClipboard, der Artikel darüber
trug also eines, das er nie benutzte, und der Abschnitt darüber trug es ebenfalls. Eine Komponente,
die einen Bedarf bekam, erzwang eine Änderung in jedem Vorfahren zwischen ihr und der Seite.
context.GetService<T>() antwortet null, wenn das Ziel die Fähigkeit nicht hat, was die Antwort
ist, die der Aufrufer jeder Fähigkeit ohnehin behandeln muss.
WOHER aufgelöst wird, ist Sache des Hosts: SSR nutzt den Container der ANFRAGE (eine Scoped-Registrierung funktioniert also, und die eigenen Registrierungen einer Seite gewinnen), der Browser nutzt, was der Boot registriert hat, eine Photon-Anwendung nutzt den der Shell. Die Komponente stellt überall dieselbe Frage.
Im WEB kam das erst in 0.2.0-preview.21 funktionierend an. Das Typargument ging bei der Transpilation verloren: Die Strategie, die den Aufruf in einen Schlüssel verwandelt, erkannte den älteren
RenderContextaus Core und einenIServiceProvider, undComponentContextist keines von beiden, der Aufruf fiel also auf den gewöhnlichen Aufrufpfad durch, der Typargumente fallen lässt. Jede Seite fragte nach einer Fähigkeit ganz ohne Namen und bekam null zurück, auf dem einen Ziel, für das es das Feature gibt.
Seit 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 ist der Ort, an den ein Abonnement gehört. Es läuft einmal, auf der Instanz, die der
Reconciler behält, und es bildet ein Paar mit OnUnmount, was verhindert, dass eine verlassene
Seite ewig weitertickt. Es erhält keinen ComponentContext, davor war der einzige Weg, in der Tiefe
eine Fähigkeit zu erreichen, also innerhalb von Build hinter einem Nur-einmal-Flag: Das Abonnement
lebte in der einen Methode, die das Framework wiederholt aufruft, und die Paarung mit OnUnmount
las sich wie ein Zufall.
Der Accessor an der Komponente antwortet genau das, was context.GetService<T>() antwortet, von
überall in der Komponente: ein Lifecycle-Hook, ein Event-Handler, Build selbst.
Seit 0.2.0-preview.33
Das C# sagt es schon, das Framework liest es also, statt noch einmal zu fragen:
| Geschrieben | Auf einem Ziel, das sie hat | Auf einem Ziel, das sie nicht hat |
|---|---|---|
IClock clock |
die Fähigkeit | ein Fehler, der die Fähigkeit und die Komponente nennt |
IClock? clock |
die Fähigkeit |
null, das die Komponente behandelt |
Ein Parameter, der nicht nullable ist, ist das Versprechen der Komponente, dass sie ohne ihn nicht
arbeiten kann, ein ihr übergebenes null scheitert also später, in Code, der Fähigkeiten nie
erwähnt, auf dem einen Ziel, auf dem der Bildschirm kaputt ist. Die Meldung nennt, welche Fähigkeit,
welche Komponente und beide Auswege: sie beim Host registrieren oder den Parameter als nullable
deklarieren. Beide Ziele sagen es, sonst wäre der Browser der nachsichtige, und der Fehler
existierte nur dort.
Eine Datei mit deaktiviertem Nullable gibt in keine Richtung ein Signal und behält die Bedeutung, die sie immer hatte.
Seit 0.2.0-preview.33
using var _ = CapabilityScope.With<IClock>(fake);
var section = new PairLoop();
section.NotifyMounted();Eine Fähigkeit, über dem, was schon gilt, bei Dispose zurückgestellt. Verschachtelung setzt sich
zusammen. Die Zeremonie, die es ersetzt, war ein von Hand geschriebener Resolver, der auf jede
ANDERE Fähigkeit, nach der die Komponente fragen könnte, null antwortete und in einem finally
rückgängig gemacht werden musste, das ein Test vergessen konnte.
Seit 0.2.0-preview.13
Dieselbe Seite muss auf dem Server renderbar sein, und der Server hat keine Kamera. Jede Fähigkeit löst dort zu einer ABWESENDEN Realisierung auf: Sie meldet sich als nicht verfügbar und gibt nichts zurück.
Ohne sie ließe sich die Seite gar nicht konstruieren: kein Konstruktor, den der Container erfüllen könnte, und die Anfrage endete in einem 500. Die eine Seite, die etwas tut, war die eine Seite, die ein Crawler nie sah, und die Besucherin wartete auf JavaScript, nur um zu erfahren, dass es die Seite gibt.
Abwesend, bewusst, und nicht simuliert. In einem Rechenzentrum gibt es keine Kamera, und das
localStorage der Besucherin liegt auf der Maschine der Besucherin, ein serverseitiger Fake wäre
also schlimmer als das Scheitern: Die Seite würde das eine rendern, der Browser das andere
hydrieren, und die Abweichung würde dem Reconciler angelastet. Die Seite nimmt den
Verfügbarkeitszweig, den sie ohnehin haben muss, und die echte Fähigkeit des Client-Boots ersetzt
den Fallback beim ersten Rendern des Clients.
INetworkStatus ist die Ausnahme, und die Ausnahme ist der Punkt: Offline zu melden würde das
Offline-Banner in das Markup einbacken, das jeder Crawler und jedes erste Bild sieht, für Besucher,
die gerade bewiesen haben, dass sie online sind, indem sie die Seite abgerufen haben. Es antwortet
online.
Mit
TryAddregistriert, eine Anwendung mit einer echten serverseitigen Antwort (ein Speicher, der von der Sitzung des Nutzers gestützt wird) registriert also ihre eigene und gewinnt.
🏁 Hier starten
🏗️ Architektur
- Architekturüberblick
- Write-once-Komponenten
- Deklarative Oberfläche
- Paketarchitektur
- Komponenten
- Styling
- Lokalisierung
- Analytics und GTM
📱 Write-once
- Photon-Engine
- Design System
- Fähigkeiten
- Speicher
- Formulare
- Code-Editor
- Markdown
- Mermaid
- E-Mail-Rendering
⚙️ Kompilierung
⚡ Runtime
🔌 Server
🎨 Ökosystem
🚀 Entwicklung