Repository navigation
Capabilities es
🌐 Esta página en: English · Português · Español · Français · Deutsch
Lo que la máquina puede hacer (cámara, ubicación, fotos, biometría, la red) como servicios que se toman por constructor, realizados por host. Un componente pide una capacidad y nunca se entera de qué destino respondió.
public sealed class ScanShell(ICamera? camera, ILocation? location) : StatefulComponent
{
private async void LocateAsync()
{
if (location is null) return; // este host no tiene
var here = await location.GetCurrentAsync();
}
}Anulable, siempre. Un host que no puede hacer algo no registra nada, así que el servicio se
resuelve a null y la aplicación muestra lo que ya sabe mostrar. Esa es la respuesta del framework
en todas partes: reportar la ausencia en lugar de fingir con un stub que falla más tarde, en un
momento peor.
| Capacidad | Qué responde | Desde |
|---|---|---|
IPhotoLibrary |
GetPermissionAsync(), PickImageAsync(), una imagen que la persona eligió |
0.2.0-preview.1 |
ICamera |
StartPreviewAsync() para una ICameraSession: una textura en vivo, y su Capture() para una foto fija |
0.2.0-preview.1 |
ILocation |
GetCurrentAsync(), y Subscribe(…) para el flujo de cambios |
0.2.0-preview.1 |
IBiometrics |
IsAvailable, AuthenticateAsync(reason): Face ID / Touch ID / lo propio de la plataforma |
0.2.0-preview.1 |
IMotionSensor |
Subscribe(…) para las lecturas de movimiento del dispositivo |
0.2.0-preview.1 |
INetworkStatus |
Current, y Subscribe(…): la conectividad y qué la transporta |
0.2.0-preview.1 |
ITextClipboard |
Read() / Write(text), para el botón Copiar propio de una página y aquello por donde un campo nativo copia, corta y pega. Todos los destinos, teléfonos incluidos |
0.2.0-preview.1 |
IThemeController |
El interruptor claro/oscuro. Vea ServerIntegration | 0.2.0-preview.1 |
IAppStorage / ISecretStore
|
Preferencias y secretos duraderos. Vea Almacenamiento | 0.2.0-preview.10 |
IClock |
Every(interval, onTick): el tiempo mismo, como algo a lo que un componente puede suscribirse |
0.2.0-preview.31 |
IFrameTicker |
OnFrame(tick): un callback antes de cada frame, con su tiempo y el delta, para lo que un estilo no puede expresar |
0.2.0-preview.46 |
IUiDispatcher |
IsOnUiThread, Post(work): el hilo desde el que se puede tocar una UI, y el camino hacia él |
0.2.0-preview.46 |
IConsent |
State, Grant(), Deny(): la respuesta de quien visita al seguimiento no esencial, la cookie de la que depende el instalador de GTM. Vea Analytics
|
0.2.0-preview.46 |
IDeepLinks |
Launch, Subscribe(onOpened): las URL CON las que se abre esta aplicación. Photon |
0.2.0-preview.47 |
IFileDialogs |
PickFile, PickFiles, PickFolder, PickSavePath: pedirle un archivo a una PERSONA. Escritorio |
0.2.0-preview.47 |
IWorkspace |
Reveal(path), OpenFile(path): entregar un ARCHIVO al sistema. Escritorio |
0.2.0-preview.49 |
IUrlLauncher |
OpenUrlAsync(url), CanOpen(url): entregar un ENLACE al sistema, por los esquemas que la aplicación abre. Photon, los teléfonos incluidos |
0.2.0-preview.64 |
IServerEvents |
Subscribe(topic, onEvent, onRefused), Connection, OnConnectionChanged(…): lo que el servidor publica en un tópico tipado, sobre una conexión por página. Web. Vea Eventos del servidor
|
0.2.0-preview.61 |
Desde 0.2.0-preview.31
Una pantalla se construye, se maqueta y se dibuja en UN solo hilo: el hilo principal de la
plataforma en cada destino nativo, y el único hilo que hay en un navegador. Todo lo demás que hace
una aplicación ocurre en otra parte: un escáner que recorre un disco, una descarga, una lectura de
base de datos. Cuando una de esas cosas termina quiere mostrar lo que encontró, y SetState es la
forma.
Eso solía ser una carrera de datos sin mensaje de error. SetState mutaba los campos del componente
desde el hilo que lo llamara, mientras el hilo de render leía ese mismo árbol: un frame dibujado con
estado medio viejo, una colección enumerada mientras crece, un fallo que se reproduce una vez por
semana.
Nada que activar. SetState le pregunta primero a IUiDispatcher. Fuera del hilo de UI la
mutación entera se encola y se ejecuta al principio del siguiente frame, antes de que se construya
nada; dentro de él, en línea, exactamente como antes. Donde no hay otro hilo no hay nada que
transferir, y la respuesta honesta es que la capacidad se reporta como ya en el hilo de UI: el
navegador tiene un solo hilo, y un servidor renderiza una petición en el hilo que la pidió.
Y la línea que no cruza. El dispatcher vuelve seguro el estado que viaja a través de SetState.
El estado compartido que un hilo de trabajo toca POR FUERA sigue siendo de la aplicación para
proteger: un modelo que la aplicación poda en un hilo de fondo, una colección a la que una tarea
añade. Una aplicación que leyera esta capacidad como "el threading ya está resuelto" y borrara su
propia guarda estaría borrando lo único que hay entre la pasada de render y un árbol que se modifica
debajo de ella. Lo que el dispatcher sí le da a esa guarda es una garantía que vale la pena: el
SetState que LEVANTA una bandera se ejecuta en línea cuando viene del hilo de UI, así que la
bandera ya está arriba antes de que empiece el trabajo.
Desde 0.2.0-preview.46
IClock es periódico a propósito (cientos de milisegundos en adelante), y su propia valla dice por
qué la animación por frame no se ofreció a través de él. IFrameTicker es ese otro contrato: un
callback antes de cada frame, que lleva el tiempo del frame y el delta desde el último del
suscriptor, en el hilo de UI y antes de que se construya el árbol, así que un SetState dentro de
un tick es lo que ese mismo frame dibuja.
protected override void OnMount() =>
_ticking = Ticker.OnFrame(tick => SetState(() => _phase += tick.DeltaMs / 1000f));
protected override void OnUnmount() => _ticking?.Dispose();Una suscripción MANTIENE LOS FRAMES FLUYENDO, que es el propósito y también el coste: libérala cuando el movimiento se detenga. En un servidor no hay frames, así que suscribirse funciona y nunca se dispara.
La mayor parte del movimiento no necesita nada de esto. Un cambio de estilo ahora se desliza solo (vea Photon); el ticker es para lo que un estilo no puede expresar: física, una simulación, geometría recalculada en cada frame a partir de datos que nunca se detienen.
Desde 0.2.0-preview.46
El acme://activate?key=… de un correo de licencia, un enlace de vuelta desde un inicio de sesión
en el navegador, un colega que pega un enlace a una pantalla. Declarar el esquema es
builder.Bundle.UrlScheme("acme") (vea
Photon); IDeepLinks es la aplicación
LEYENDO con qué se abrió. Sin las dos mitades un deep link abre la aplicación por su puerta
principal y suelta lo que traía, lo que para quien hizo clic se ve exactamente como un enlace roto.
protected override void OnMount()
{
if (Links.Launch is { } opened) Activate(opened); // lo que ARRANCÓ esta aplicación
_link = Links.Subscribe(Activate); // y cada uno de ahora en adelante
}
protected override void OnUnmount() => _link?.Dispose();Haz las dos cosas, y este es el porqué. Launch no se puede leer en un constructor en macOS,
medido y no supuesto. La URL de arranque llega como un AppleEvent, los AppleEvents los entrega EL
run loop, y el run loop arranca después de que se construye el primer árbol, así que un arranque en
frío llega a un SUSCRIPTOR y encuentra la propiedad todavía nula un momento antes. La suscripción es
lo que atrapa un arranque en frío; la propiedad es para preguntar después, en una pantalla
construida más tarde. Una aplicación que hace las dos cosas acierta en todas partes.
Launch no se vuelve a entregar a un suscriptor, así que una aplicación que hace las dos cosas
actúa una vez. Una URL que la aplicación no RECONOCE se entrega igual: decidir qué significa un host
desconocido es cosa de la aplicación, y tragársela sería decidir en su nombre sin dejar rastro. Una
cadena que no es una URL se descarta.
En un teléfono (Desde 0.2.0-preview.63) las mismas dos líneas funcionan, y el mismo
builder.Bundle.UrlScheme("acme") es la declaración entera: el build escribe el tipo de URL que un
iPhone lee y el filtro de enlaces que Android compara, y nadie abre un Info.plist ni un
manifiesto.
- Un teléfono entrega el enlace CON el arranque, así que ahí
Launchya está puesto cuando se construye el primer árbol, y es exacto: una aplicación abierta desde su icono respondenull, y un enlace tocado una hora después lo oye un suscriptor y no pasa a ser ese valor. - Un enlace abierto mientras la aplicación corre llega a la aplicación en ejecución. En Android una aplicación que declara un esquema es una sola tarea, así que el sistema le entrega el enlace en lugar de arrancar una segunda copia encima, y solo se entrega un enlace de un esquema que la aplicación declaró, lo envíe quien lo envíe.
- Estos son los esquemas propios de la aplicación. Un enlace
httpsque abre la aplicación sin preguntar nada (Universal Links, App Links) necesita también la palabra del sitio, y todavía no está.
Solo Photon: en la web un deep link es la barra de direcciones y el router ya es su dueño.
Desde 0.2.0-preview.47
La única forma en que una aplicación en sandbox toca algo que no se le dio, y la razón por la que
existe PhotonEntitlements.UserSelectedFiles.
var folder = await Dialogs.PickFolder("Choose a folder to scan");
var report = await Dialogs.PickSavePath("report.csv",
filters: [new FileFilter("Spreadsheets", "csv")]);Null significa que la persona CANCELÓ, que es una respuesta corriente y no una excepción: quien
cierra un diálogo no ha fallado en nada, y una aplicación que tiene que capturar para enterarse es
una aplicación que se va a olvidar de hacerlo. PickFiles responde con una lista vacía por la misma
razón: quien está a punto de iterar no debería tener que preguntar dos veces.
Llama desde cualquier parte. Los paneles son modales y solo del hilo principal, y la realización
pasa ella misma por IUiDispatcher, porque un panel abierto desde un hilo de trabajo no se abre en
absoluto: simplemente nunca aparece, sin nada en el registro.
Una ruta vale para ESTA EJECUCIÓN. En una aplicación en sandbox el permiso que una persona concedió al elegir un archivo no sobrevive a un reinicio; eso necesita un marcador con ámbito de seguridad (security-scoped bookmark), que es otra moneda y todavía no se ofrece. Una aplicación que guarda la ruta y la lee en el siguiente arranque es rechazada sin explicación. Solo escritorio: el selector de un teléfono responde con una URL con ámbito y no con una ruta.
Desde 0.2.0-preview.47
La otra mitad de IFileDialogs: aquel le pide una ruta a una persona, este le devuelve una ruta al
sistema. Lo que hace un escáner de disco después de encontrar al culpable, lo que hace una aplicación
con el informe que acaba de escribir y que la persona quiere en su hoja de cálculo.
Workspace.Reveal(folder); // mostrar dónde ESTÁ, seleccionado, sin abrir
Workspace.OpenFile(reportPath); // entregarlo a quien sea dueño de ese tipo de archivoCada uno responde si el sistema lo aceptó. false es una respuesta corriente y no una excepción:
una ruta puede desaparecer entre un escaneo y un clic. La línea es QUIÉN se equivocó: que el sistema
rechace algo que se podía haber preguntado es false; una ruta relativa, que a ningún sistema se le
podría haber entregado, es error de quien llama y lanza. Reveal muestra un archivo o una carpeta
seleccionados en el explorador de archivos sin abrirlos; OpenFile toma un ARCHIVO y lo entrega a
quien es dueño de su tipo. Solo escritorio, por la misma razón que los diálogos: un teléfono tiene
una hoja de compartir y un proveedor de documentos, que no son esto.
Desde 0.2.0-preview.49
Un enlace se le entrega al sistema igual en un escritorio y en un teléfono: una pantalla de ajustes con un enlace a su propia página de privacidad, un botón "Contactar con soporte", la dirección de una tienda abierta en el mapa de la persona.
await Links.OpenUrlAsync(new Uri("https://acme.example/privacy")); // un navegador
await Links.OpenUrlAsync(new Uri("mailto:support@acme.example")); // la app de correo, una vez que Program.cs abre mailto
if (Links.CanOpen(acmeLink)) … // una PREGUNTA: sin diálogo, sin efecto secundarioCada uno responde si el sistema lo aceptó, y false es una respuesta corriente: un esquema puede no
tener nada detrás en esta máquina. Una URL relativa, que a ningún sistema se le podría haber
entregado, es error de quien llama y lanza. Abrir es asíncrono porque un iPhone dice si abrió el
enlace en un callback, más tarde; un escritorio y Android responden en el acto, y la tarea ya viene
terminada.
Cada shell de Photon lo realiza. El navegador no registra ninguno: allí un enlace es un ancla, y el
navegador ya lo enruta. Eran IWorkspace.OpenUrl e IWorkspace.CanOpen hasta que un teléfono los
necesitó, y un contrato que también muestra un archivo en un explorador de archivos es uno que un
teléfono nunca podría registrar.
Desde 0.2.0-preview.64
Una URL se entrega por esquema, y solo por los esquemas que tu aplicación abre. Entregar una URL
al sistema operativo la entrega a quien reclame su esquema: https llega a un navegador, que es lo
que significa "abrir este enlace"; file lanza lo que la ruta nombra; otros esquemas abren sesiones
o ejecutan la aplicación que los haya registrado. Un enlace que llegó en contenido (un elemento de
un feed, un valor con el que respondió un servidor) nunca debe poder elegir entre esos, así que cada
realización consulta una OpenUrlPolicy antes que a la plataforma. Por defecto entrega http,
https y cada esquema al que la aplicación declaró responder con builder.Bundle.UrlScheme(…): el
SDK los conoce, así que nadie los dice dos veces. Cualquier otra cosa es una línea en Program.cs:
builder.Workspace.OpensMail(); // mailto:, un botón "Contactar con soporte"
builder.Workspace.OpensPhone().OpensMessages(); // tel:, sms:
builder.Workspace.Opens("x-apple.systempreferences"); // el de otra aplicación, o el del sistema, por nombreUna URL rechazada responde false, exactamente como hace el sistema con un esquema que nadie
reclama, y la realización registra el esquema y la única línea que lo abriría. No es una excepción,
porque la política existe para las URL que la aplicación no escribió, y un enlace hostil tiene que
ser algo que un manejador de clic pueda entregar sin un try/catch. CanOpen aplica la misma
política, así que un botón condicionado por él desaparece justo para los enlaces que OpenUrlAsync
rechazaría. file no se puede habilitar: un archivo tiene puertas tipadas, OpenFile y Reveal,
que comprueban que la ruta existe y nunca lanzan una carpeta. Una aplicación con reglas propias
registra su propia OpenUrlPolicy antes de Build(), y la del builder se aparta.
Desde 0.2.0-preview.50
Un teléfono responde CanOpen solo para los esquemas por los que el manifiesto de la aplicación dice
que puede preguntar: LSApplicationQueriesSchemes en iOS, y queries desde Android 11, donde a una
aplicación que no enumera nada se le dice que nada abre ni siquiera un enlace https. El build
escribe los dos a partir de las mismas líneas builder.Workspace.Opens… con las que se construye la
política (con los dos esquemas de la web en Android), así que la pregunta dice la verdad y nadie abre
un Info.plist ni un manifiesto. Por eso también un esquema abierto ahí es una constante: uno
construido en tiempo de ejecución no alcanza un manifiesto escrito en tiempo de build, y el build se
detiene en esa línea (EQ3007). Una aplicación que registra su propia
OpenUrlPolicy tiene sus líneas Allowing(…) leídas de la misma manera.
Lo que abre cada uno:
-
tel:el marcador del teléfono, con el número en él. La llamada la hace la persona. -
sms:la app de mensajes, ymailto:la app de correo. - Un lugar, como enlace de mapa:
https://maps.apple.com/?q=…abre Mapas en un iPhone, ygeo:0,0?q=…(conOpens("geo")) la app de mapas en Android. Una llamada que abra un lugar en cualquier sistema todavía no existe.
El simulador de iOS no tiene la app Teléfono ni Mail, así que allí tel: y mailto: responden
false.
ILocation, IMotionSensor e INetworkStatus son las que tienen una respuesta que CAMBIA, así que
devuelven un IDisposable desde Subscribe en lugar de un valor. Libéralo cuando el componente se
vaya, o la suscripción sobrevive a lo que estaba actualizando.
IClock es la que tiene una respuesta que NO ES MÁS QUE cambio, y es lo que le faltaba a una
pantalla que avanza sola: un carrusel, un contador en vivo, un estado que hace polling. Todo lo
demás de aquí reacciona a algo que hizo una persona.
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();
}El par es el contrato: OnMount se suscribe, OnUnmount libera, y sin la segunda mitad cada
navegación deja un temporizador corriendo contra un componente que nadie puede ver. En un SERVIDOR
el reloj nunca hace tick, que es la respuesta correcta y no una que falte: un servidor renderiza un
frame, así que el primer pintado es el estado con el que se construyó el componente y los ticks
empiezan en la hidratación. Un teléfono que pasó diez minutos en un bolsillo vuelve a UN tick, nunca
a seiscientos.
Es un reloj PERIÓDICO y no un reloj de frames, a propósito. La animación por frame necesita dos
cosas que todavía no existen, la retención de estado posicional a través del reconciliador y un
canal de geometría en StyleChannels, y un callback a 60 Hz sin ellas sería la forma más lenta de
animar.
INetworkStatus reporta la conectividad tal como la ve la plataforma, no una promesa de que un
host concreto vaya a responder. Una aplicación que muestra un "en línea" caducado es peor que una
que no dice nada.
PermissionState tiene cuatro respuestas, y la tercera es la que las aplicaciones olvidan:
-
NotDetermined: nunca se preguntó. Preguntar muestra el aviso del sistema. -
Granted. -
Denied: rechazado, y volver a preguntar no hace nada. Solo los ajustes del sistema pueden cambiarlo, así que un botón que vuelve a preguntar es un botón que no hace nada; envía a la persona a los ajustes en su lugar. - La plataforma también puede reportar una restricción que el usuario no puede levantar en absoluto.
En nativo, los textos que el sistema operativo muestra en el aviso vienen del ensamblado:
[assembly: PhotonCapability("camera", "Scan a barcode to add an item.")]El build los lee hacia el manifiesto de la plataforma (Info.plist, el manifiesto de Android), así
que la razón que lee un usuario se enuncia una vez, en la aplicación, junto al código que la
necesita.
Cruza por NOMBRE, nunca por ordinal. El manifiesto compara la cadena de la capacidad. Insertar un valor en medio de un enum convirtió una vez Location en Motion en un manifiesto publicado: el build estaba en verde y la aplicación pidió el permiso equivocado.
Un componente nunca pregunta en qué destino está. Quien elige destinos necesita saber igualmente qué entrega cada uno, así que esta es la respuesta por contrato, para los shells nativos:
| Contrato | macOS | iOS | Android | Windows |
|---|---|---|---|---|
IPhotoLibrary |
✓ | ✓ | ✓ | ✓ |
ICamera |
✓ | ✓ | ✓ | no disponible |
ILocation |
✓ | ✓ | ✓ | no disponible |
IBiometrics |
✓ | ✓ | ✓ | no disponible |
IMotionSensor |
✓ | ✓ | ✓ | no disponible |
INetworkStatus |
✓ | ✓ | ✓ | ✓ |
ITextClipboard |
✓ | ✓ | ✓ | ✓ |
IAppStorage |
✓ | ✓ | ✓ | ✓ |
ISecretStore |
✓ | ✓ | ✓ | ✓ |
IDeepLinks |
✓ | ✓ | ✓ | ✓ |
IFileDialogs |
✓ | null |
null |
✓ |
IWorkspace |
✓ | null |
null |
✓ |
IUrlLauncher |
✓ | ✓ | ✓ | ✓ |
-
✓: el shell tiene una realización. Si ESTA máquina tiene el hardware sigue siendo la respuesta
propia del contrato:
IMotionSensor.IsAvailableesfalseen un Mac, que tiene la API y ningún sensor. -
no disponible: el shell registra un objeto con un
IsAvailableque siempre esfalse, así que una página escrita para un teléfono igualmente se construye ahí y muestra su rama "no en este dispositivo". -
null: el shell no registra nada, y el servicio se resuelve anull.IFileDialogseIWorkspaceson contratos de escritorio, y esta es su respuesta en un teléfono.
El navegador es la siguiente sección.
ICamera responde en Android como lo hace en iOS (Desde 0.2.0-preview.62). Hasta entonces
UseCamera(reason) escribía el permiso en el manifiesto y el shell no registraba nada, así que un
CameraPreview dibujaba su estado no disponible en el único teléfono al que le faltaba.
public sealed class ScanPage(ICamera? camera) : StatefulComponent
{
private ICameraSession? _session;
private async void StartAsync()
{
if (camera is null) return;
var session = await camera.StartPreviewAsync(); // pregunta mientras el sistema aún lo hace, null si se rechaza
SetState(() => _session = session);
}
public override VisualNode Build(ComponentContext context) =>
CameraPreview(_session, 320, 320, cornerRadius: new CornerRadii(24), label: "Scanner");
}-
El permiso está dentro de
StartPreviewAsync. Muestra el aviso del sistema siempre que el sistema todavía muestre uno, y respondenullcuando la persona lo rechaza.PermissionleeDeniedsolo cuando volver a preguntar no mostraría nada, que es lo que la palabra significa en cada destino: en iOS eso es el primer rechazo, y en Android 11 y posteriores es el segundo, así que después de un rechazo ahí todavía leeNotDeterminedy una segunda pregunta muestra el aviso. Android por sí mismo solo dice "concedido" o "no": el shell recuerda la última respuesta, que es lo que distingue un permiso nunca pedido de uno rechazado para siempre. - Los frames llegan como píxeles, derechos para la forma en que se sostiene el dispositivo. La vista previa es un nodo corriente: recorta, toma un radio de esquina y queda debajo de lo que se dibuje encima.
-
Capture()devuelve el frame en pantalla como una foto fija codificada (image/jpegen Android). - La cámara se cierra cuando la aplicación sale de la pantalla y se abre otra vez cuando vuelve, en la misma sesión. Una aplicación que solo está en pausa y sigue a la vista, junto a otra ventana o debajo de un aviso del sistema, conserva su vista previa.
El navegador realiza lo que honestamente puede (fotos a través de un input de archivo, cámara, ubicación y red a través de sus API web) y registra el resto como no disponible en lugar de ausente, así que una página que toma una capacidad igualmente recibe un objeto y muestra su alternativa en lugar de fallar al construirse.
Una página toma lo que necesita por su constructor en cada destino:
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");
}De forma nativa lo resuelve ActivatorUtilities. En el navegador el constructor transpilado lo
resuelve él mismo, por el NOMBRE de la interfaz: un tipo de C# no existe ahí en tiempo de ejecución,
pero IPhotoLibrary como cadena sí, y los dos lados coinciden en ella.
Sea cual sea la forma de constructor que lo declare. Un constructor primario tomaba un camino
distinto por el parser hasta 0.2.0-preview.33, y una sección escrita PairLoop(IClock clock)
recibía if (clock !== undefined) this.clock = clock en su lugar: nadie que la componga en medio de
un árbol pasa un reloj, así que el campo se quedaba undefined y el componente estaba inerte, en
silencio, en un solo destino.
Tampoco lo pasa nadie. La dependencia sale del constructor emitido, así que también sale del SITIO
DE LLAMADA y de la factory generada: Quark(mood, size) es la firma entera que ve quien llama, y
new Quark(clock, mood, size) en C# suelta el reloj al salir en lugar de desplazar un lugar cada
argumento que le sigue. La regla se le pregunta al modelo, no se adivina por un nombre: un parámetro
de constructor con un tipo que es una INTERFAZ es una dependencia, todo lo demás son datos que pasa
quien llama. Un componente toma lo que dibuja (una etiqueta, una variante, un callback) y nada de
eso es nunca una interfaz. (IReadOnlyList<T> e IEnumerable<T> son datos, explícitamente.)
Desde 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(); // aquí no hay portapapeles, así que no dibujar nada en lugar de un botón muerto
}La inyección por constructor sigue siendo la mejor respuesta donde encaja: explícita, testeable,
legible en la firma. Pero solo llega hasta la PÁGINA. A todo lo de abajo había que pasarle lo mismo
a mano: una tarjeta con un botón Copiar necesita un ITextClipboard, así que el artículo de encima
cargaba con uno que nunca usaba, y la sección de encima de ese también. Un componente que ganaba una
necesidad forzaba una edición en cada ancestro entre él y la página.
context.GetService<T>() responde null cuando el destino no tiene la capacidad, que es la respuesta
que quien llama a cualquier capacidad tiene que manejar de todos modos.
DESDE dónde se resuelve es asunto del host: el SSR usa el contenedor de la PETICIÓN (así que un registro con ámbito funciona y los registros propios de una página ganan), el navegador usa lo que registró el arranque, una aplicación Photon usa el del shell. El componente hace la misma pregunta en todas partes.
En la WEB esto llegó funcionando solo en 0.2.0-preview.21. El argumento de tipo se perdía en la transpilación: la estrategia que convierte la llamada en una clave reconocía el
RenderContextmás antiguo de Core y unIServiceProvider, yComponentContextno es ninguno de los dos, así que la llamada caía al camino de invocación corriente, que suelta los argumentos de tipo. Cada página pedía una capacidad sin nombre alguno y recibía null, en el único destino al que la funcionalidad existe para servir.
Desde 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 es donde corresponde una suscripción. Se ejecuta una vez, en la instancia que el
reconciliador conserva, y hace pareja con OnUnmount, que es lo que impide que una página de la que
se navegó fuera siga haciendo tick para siempre. No recibe ningún ComponentContext, así que antes
de esto la única forma de alcanzar una capacidad en profundidad era dentro de Build detrás de una
bandera de una sola vez: la suscripción vivía en el único método que el framework llama
repetidamente, y la pareja con OnUnmount se leía como un accidente.
El accesor del componente responde exactamente lo que responde context.GetService<T>(), desde
cualquier parte del componente: un hook de ciclo de vida, un manejador de eventos, el propio
Build.
Desde 0.2.0-preview.33
El C# ya lo dice, así que el framework lo lee en lugar de volver a preguntar:
| Escrito | En un destino que la tiene | En un destino que no |
|---|---|---|
IClock clock |
la capacidad | un error que nombra la capacidad y el componente |
IClock? clock |
la capacidad |
null, para que el componente lo maneje |
Un parámetro no anulable es el componente prometiendo que no puede funcionar sin él, así que un
null que se le entrega falla más tarde, dentro de código que nunca menciona capacidades, en el
único destino donde la pantalla está rota. El mensaje nombra qué capacidad, qué componente y las dos
salidas: registrarla en el host o declarar el parámetro como anulable. Los dos destinos lo dicen, o
el navegador sería el indulgente y el error existiría solo ahí.
Un archivo con nullable desactivado no da señal en ningún sentido, y conserva el significado que siempre tuvo.
Desde 0.2.0-preview.33
using var _ = CapabilityScope.With<IClock>(fake);
var section = new PairLoop();
section.NotifyMounted();Una capacidad, por encima de lo que ya esté en vigor, devuelta a su sitio al liberar. El anidamiento
compone. La ceremonia que reemplaza era un resolver escrito a mano, que respondía null a
cualquier OTRA capacidad que el componente pudiera pedir y había que deshacer en un finally que
una prueba podía olvidar.
Desde 0.2.0-preview.13
La misma página tiene que ser renderizable en el servidor, y el servidor no tiene cámara. Cada capacidad se resuelve ahí a una realización AUSENTE: se reporta como no disponible y no devuelve nada.
Sin ella la página no se podría construir en absoluto: ningún constructor que el contenedor pudiera satisfacer, y la petición terminaba en un 500. La única página que hace algo era la única página que un crawler nunca veía, y quien visitaba esperaba al JavaScript solo para que le dijeran que la página existe.
Ausente, a propósito, y no simulada. No hay cámara en un centro de datos y el localStorage de
quien visita está en la máquina de quien visita, así que un fake del lado del servidor sería peor
que el fallo: la página renderizaría una cosa, el navegador hidrataría otra, y la diferencia se le
achacaría al reconciliador. La página toma la rama de disponibilidad que ya tiene que tener, y la
capacidad real del arranque del cliente reemplaza la alternativa en el primer render del cliente.
INetworkStatus es la excepción, y la excepción es la clave: reportar sin conexión incrustaría el
banner de sin conexión en el marcado que ve cada crawler y cada primer pintado, para visitantes que
acaban de demostrar que están en línea al traer la página. Responde en línea.
Registrada con
TryAdd, así que una aplicación con una respuesta genuina del lado del servidor (un almacenamiento respaldado por la sesión del usuario) registra la suya y gana.
🏁 Empieza aquí
🏗️ Arquitectura
- Visión general de la arquitectura
- Componentes write-once
- Superficie declarativa
- Arquitectura de paquetes
- Componentes
- Estilos
- Localización
- Analytics y GTM
📱 Write-once
- Motor Photon
- Design System
- Capacidades
- Almacenamiento
- Formularios
- Editor de código
- Markdown
- Mermaid
- Renderizado de email
⚙️ Compilación
⚡ Runtime
🔌 Servidor
🎨 Ecosistema
🚀 Desarrollo