Repository navigation
Capabilities
🌐 This page in: English · Português
What the machine can do (camera, location, photos, biometrics, the network) as services taken through a constructor, realized per host. A component asks for a capability and never learns which target answered.
public sealed class ScanShell(ICamera? camera, ILocation? location) : StatefulComponent
{
private async void LocateAsync()
{
if (location is null) return; // this host has none
var here = await location.GetCurrentAsync();
}
}Nullable, always. A host that cannot do something registers nothing, so the service resolves to
null and the app shows the thing it already knows how to show. That is the framework's answer
everywhere: report the absence rather than pretend with a stub that fails later, at a worse moment.
| Capability | What it answers | Since |
|---|---|---|
IPhotoLibrary |
GetPermissionAsync(), PickImageAsync(), one picture the user chose |
0.2.0-preview.1 |
ICamera |
Capture() for a still, StartPreviewAsync() for an ICameraSession (a live texture) |
0.2.0-preview.1 |
ILocation |
GetCurrentAsync(), and Subscribe(…) for the stream of changes |
0.2.0-preview.1 |
IBiometrics |
IsAvailable, AuthenticateAsync(reason): Face ID / Touch ID / the platform's own |
0.2.0-preview.1 |
IMotionSensor |
Subscribe(…) for device motion readings |
0.2.0-preview.1 |
INetworkStatus |
Current, and Subscribe(…): reachability and what carries it |
0.2.0-preview.1 |
ITextClipboard |
Read() / Write(text), for a page's own Copy button |
0.2.0-preview.1 |
IThemeController |
The light/dark switch. See ServerIntegration | 0.2.0-preview.1 |
IAppStorage / ISecretStore
|
Durable preferences and secrets. See Storage | 0.2.0-preview.10 |
IClock |
Every(interval, onTick): time itself, as something a component can subscribe to |
0.2.0-preview.31 |
IFrameTicker |
OnFrame(tick): a callback before every frame, with its time and the delta — for what a style cannot express |
0.2.0-preview.46 |
IUiDispatcher |
IsOnUiThread, Post(work): the thread a UI may be touched from, and the way onto it |
0.2.0-preview.46 |
IConsent |
State, Grant(), Deny(): the visitor's answer to non-essential tracking, the cookie the GTM installer gates on. See Analytics
|
0.2.0-preview.46 |
IDeepLinks |
Launch, Subscribe(onOpened): the URLs this app is opened WITH. Photon |
0.2.0-preview.47 |
IFileDialogs |
PickFile, PickFiles, PickFolder, PickSavePath: asking a PERSON for a file. Desktop |
0.2.0-preview.47 |
IWorkspace |
Reveal(path), OpenFile(path), OpenUrl(url), CanOpen(url): handing a path or a link to the SYSTEM, by the schemes the app opens. Desktop |
0.2.0-preview.49 |
IServerEvents |
Subscribe(topic, onEvent, onRefused), Connection, OnConnectionChanged(…): what the server publishes to a typed topic, over one connection per page. Web. See Server events
|
0.2.0-preview.61 |
Since 0.2.0-preview.31
A screen is built, laid out and drawn on ONE thread — the platform's main thread on every native
target, and the only thread there is in a browser. Everything else an app does happens elsewhere: a
scanner walking a disk, a download, a database read. When one of those finishes it wants to show
what it found, and SetState is how.
That used to be a data race with no error message. SetState mutated the component's fields from
whatever thread called it, while the render thread was reading that very tree: a frame drawn from
half-old state, a collection enumerated while it grows, a crash that reproduces once a week.
Nothing to opt into. SetState asks IUiDispatcher first. Off the UI thread the whole
mutation is posted and runs at the top of the next frame, before anything is built; on it, inline,
exactly as before. Where there is no other thread there is nothing to marshal, and the honest
answer is that the capability reports itself already on the UI thread — the browser has one
thread, and a server renders a request on the thread that asked.
And the line it does not cross. The dispatcher makes safe the state that travels through
SetState. Shared state a worker thread touches OUTSIDE it is still the app's own to guard — a
model the app prunes on a background thread, a collection a task appends to. An app that read this
capability as "threading is handled now" and deleted its own guard would be deleting the only thing
standing between the render pass and a tree being modified under it. What the dispatcher does give
that guard is a guarantee worth having: the SetState that RAISES a flag runs inline when it comes
from the UI thread, so the flag is already up before the work starts.
Since 0.2.0-preview.46
IClock is deliberately periodic — hundreds of milliseconds and up — and its own fence says why
per-frame animation was not offered through it. IFrameTicker is that other contract: a callback
before every frame, carrying the frame's time and the delta since the subscriber's last one, on the
UI thread and before the tree is built, so a SetState inside a tick is what that very frame draws.
protected override void OnMount() =>
_ticking = Ticker.OnFrame(tick => SetState(() => _phase += tick.DeltaMs / 1000f));
protected override void OnUnmount() => _ticking?.Dispose();A subscription KEEPS FRAMES FLOWING, which is the point and also the cost: dispose when the motion stops. On a server there are no frames, so subscribing succeeds and never fires.
Most motion needs none of this. A style change glides on its own now (see Photon); the ticker is for what a style cannot express — physics, a simulation, geometry recomputed every frame from data that never stops.
Since 0.2.0-preview.46
A licence e-mail's acme://activate?key=…, a link back from a browser sign-in, a colleague pasting
a link to one screen. Declaring the scheme is builder.Bundle.UrlScheme("acme") (see
Photon); IDeepLinks is the app READING what
it was opened with. Without both halves a deep link opens the app at its front door and drops what
it was carrying, which to the person who clicked it looks exactly like a broken link.
protected override void OnMount()
{
if (Links.Launch is { } opened) Activate(opened); // what STARTED this app
_link = Links.Subscribe(Activate); // and every one from now on
}
protected override void OnUnmount() => _link?.Dispose();Do both, and here is why. Launch is not readable in a constructor on macOS — measured rather
than assumed. The launch URL arrives as an AppleEvent, AppleEvents are delivered BY the run loop,
and the run loop starts after the first tree is built, so a cold launch reaches a SUBSCRIBER and
finds the property still null a moment earlier. The subscription is what catches a cold launch; the
property is for asking later, on a screen built after. An app that does both is right everywhere.
Launch is not replayed to a subscriber, so an app that does both acts once. A URL the app does not
RECOGNISE is still delivered — deciding what an unknown host means is the app's, and swallowing it
would decide on the app's behalf and leave no evidence. A string that is not a URL is dropped.
Photon only: on the web a deep link is the address bar and the router already owns it.
Since 0.2.0-preview.47
The one way a sandboxed app touches anything it was not given, and the reason
PhotonEntitlements.UserSelectedFiles exists.
var folder = await Dialogs.PickFolder("Choose a folder to scan");
var report = await Dialogs.PickSavePath("report.csv",
filters: [new FileFilter("Spreadsheets", "csv")]);Null means the person CANCELLED, which is an ordinary answer rather than an exception: someone
closing a dialog has not failed at anything, and an app that must catch to find out is an app that
will forget to. PickFiles answers with an empty list for the same reason — a caller about to loop
should not have to ask twice.
Call from anywhere. The panels are modal and main-thread-only, and the realization marshals through
IUiDispatcher itself, because a panel opened from a worker thread does not open at all — it simply
never appears, with nothing logged.
A path is good for THIS RUN. In a sandboxed app the permission a person granted by choosing a file does not survive a relaunch; that needs a security-scoped bookmark, which is a different currency and is not offered yet. An app that stores the path and reads it next launch is refused with no explanation. Desktop only: a phone's picker answers with a scoped URL and not a path.
Since 0.2.0-preview.47
The other half of IFileDialogs: that one asks a person for a path, this one hands a path — or a
link — back to the system. What a disk scanner does after it found the offender, what a settings
screen does with a link to its own privacy page, what an app does with the report it just wrote and
the person wants in their spreadsheet.
Workspace.Reveal(folder); // show where it IS, selected, not opened
Workspace.OpenFile(reportPath); // hand it to whatever owns that kind of file
Workspace.OpenUrl(new Uri("https://…")); // a browser, for this one
if (Workspace.CanOpen(acmeLink)) … // a QUESTION: no dialog, no side effectEach answers whether the system took it. false is an ordinary answer and not an exception: a path
can vanish between a scan and a click, and a scheme can have nothing behind it on this machine. The
line is WHO was wrong — the system declining something that could have been asked is false; a
relative path or a relative URL, which no system could have been handed, is the caller's mistake and
throws. Reveal shows a file or folder selected in the file browser without opening it; OpenFile
takes a FILE and hands it to what owns its kind. Desktop only, for the same reason as the dialogs: a
phone has a share sheet and a document provider, which are not these.
Since 0.2.0-preview.49
A URL is handed over by scheme, and only the schemes your app opens. Handing a URL to the
operating system hands it to whatever claims its scheme: https reaches a browser, which is what
"open this link" means; file launches what the path names; other schemes open sessions or run
whichever app registered them. A link that arrived in content — a feed item, a value a server
answered with — must never get to choose among those, so every realization consults one
OpenUrlPolicy before the platform. By default it hands over http, https, and every scheme the
app declared it answers to with builder.Bundle.UrlScheme(…) — the SDK knows those, so nobody says
them twice. Anything else is a line in Program.cs:
builder.Workspace.OpensMail(); // mailto: — a "Contact support" button
builder.Workspace.OpensPhone().OpensMessages(); // tel:, sms:
builder.Workspace.Opens("x-apple.systempreferences"); // another app's, or the system's, by nameA refused URL answers false, exactly as the system does for a scheme nothing claims, and the
realization logs the scheme and the one line that would open it — not an exception, because the
policy exists for URLs the app did not write, and a hostile link has to be something a click handler
can hand over without a try/catch. CanOpen applies the same policy, so a button gated by it
disappears for exactly the links OpenUrl would refuse. file cannot be opted in: a file has typed
doors, OpenFile and Reveal, which check that the path exists and never launch a folder. An app
with its own rules registers its own OpenUrlPolicy before Build(), and the builder's steps aside.
Since 0.2.0-preview.50
ILocation, IMotionSensor and INetworkStatus are the ones whose answer CHANGES, so they hand
back an IDisposable from Subscribe rather than a value. Dispose it when the component goes away,
or the subscription outlives what it was updating.
IClock is the one whose answer is nothing BUT change, and it is what a screen that advances itself
was missing: a carousel, a live counter, a status that polls. Everything else here reacts to
something a person did.
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();
}The pair is the contract: OnMount subscribes, OnUnmount disposes, and without the second half
every navigation leaves a timer running against a component nobody can see. On a SERVER the clock
never ticks, which is the right answer rather than a missing one: a server renders one frame, so the
first paint is the state the component was built with and the ticking starts at hydration. A phone
that spent ten minutes in a pocket comes back to ONE tick, never to six hundred.
It is a PERIODIC clock and not a frame clock, deliberately. Per-frame animation needs two things
that do not exist yet, positional state retention through the reconciler and a geometry channel in
StyleChannels, and a 60Hz callback without them would be the slowest way to animate.
INetworkStatus reports reachability as the platform sees it, not a promise a specific host will
answer. An app showing a stale "online" is worse than one that says nothing.
PermissionState has four answers, and the third is the one apps forget:
-
NotDetermined: never asked. Asking shows the system prompt. -
Granted. -
Denied: refused, and asking again does nothing. Only the system's settings can change it, so a button that re-asks is a button that does nothing; send them to settings instead. - The platform may also report a restriction the user cannot lift at all.
On native, the strings the OS shows at the prompt come from the assembly:
[assembly: PhotonCapability("camera", "Scan a barcode to add an item.")]The build reads those into the platform's manifest (Info.plist, the Android manifest), so the
reason a user reads is stated once, in the app, next to the code that needs it.
Crosses by NAME, never by ordinal. The manifest matches the capability's string. Inserting a value into the middle of an enum once turned Location into Motion in a shipped manifest: the build was green and the app asked for the wrong permission.
The browser realizes what it honestly can (photos through a file input, camera and location and network through their web APIs) and registers the rest as unavailable rather than absent, so a page that takes one still receives an object and shows its fallback instead of failing to construct.
A page takes what it needs through its constructor on every target:
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");
}Natively ActivatorUtilities resolves it. In the browser the transpiled constructor resolves it
itself, by the interface's NAME: a C# type does not exist at run time there, but IPhotoLibrary as
a string does, and both sides agree on it.
Whichever constructor form declares it. A primary constructor took a different path through the
parser until 0.2.0-preview.33, and a section written PairLoop(IClock clock) got
if (clock !== undefined) this.clock = clock instead: nobody composing it in the middle of a tree
passes a clock, so the field stayed undefined and the component was inert, in silence, on one
target only.
Nobody passes it, either. The dependency leaves the emitted constructor, so it also leaves the
CALL SITE and the generated factory: Quark(mood, size) is the whole signature a caller sees, and
new Quark(clock, mood, size) in C# drops the clock on the way out rather than sliding every
argument after it one place along. The rule is asked of the model, not guessed from a name: a
constructor parameter whose type is an INTERFACE is a dependency, everything else is data the caller
passes. A component takes what it draws (a label, a variant, a callback) and none of those is ever
an interface. (IReadOnlyList<T> and IEnumerable<T> are data, explicitly.)
Since 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(); // no clipboard here, so draw nothing rather than a dead button
}Constructor injection stays the better answer where it fits: explicit, testable, readable in the
signature. But it only reaches the PAGE. Everything below had to be handed the same thing by hand:
a card with a Copy button needs an ITextClipboard, so the article above it carried one it never
used, and the section above that carried it too. A component that gained a need forced an edit in
every ancestor between it and the page.
context.GetService<T>() answers null when the target does not have the capability, which is the
answer every capability's caller has to handle anyway.
Where it resolves FROM is the host's business: SSR uses the REQUEST's container (so a scoped registration works and a page's own registrations win), the browser uses what the boot registered, a Photon app uses the shell's. The component asks the same question everywhere.
On the WEB this arrived working only in 0.2.0-preview.21. The type argument was dropped in transpilation: the strategy that turns the call into a key recognised the older Core
RenderContextand anIServiceProvider, andComponentContextis neither, so the call fell through to the ordinary invocation path, which drops type arguments. Every page asked for a capability by no name at all and got null back, on the one target the feature exists to serve.
Since 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 is where a subscription belongs. It runs once, on the instance the reconciler keeps, and
it pairs with OnUnmount, which is what stops a navigated away page ticking forever. It receives no
ComponentContext, so before this the only way to reach a capability at depth was inside Build
behind a run once flag: the subscription lived in the one method the framework calls repeatedly, and
the pairing with OnUnmount read as an accident.
The accessor on the component answers exactly what context.GetService<T>() answers, from anywhere
in the component: a lifecycle hook, an event handler, Build itself.
Since 0.2.0-preview.33
The C# already says it, so the framework reads it rather than asking again:
| Written | On a target that has it | On a target that does not |
|---|---|---|
IClock clock |
the capability | an error naming the capability and the component |
IClock? clock |
the capability |
null, for the component to handle |
A non nullable parameter is the component promising it cannot work without one, so a null handed
to it fails later, inside code that never mentions capabilities, on the one target where the screen
is broken. The message names which capability, which component, and both ways out: register it with
the host, or declare the parameter nullable. Both targets say it, or the browser would be the
lenient one and the bug would exist only there.
A file with nullable disabled gives no signal either way, and keeps the meaning it always had.
Since 0.2.0-preview.33
using var _ = CapabilityScope.With<IClock>(fake);
var section = new PairLoop();
section.NotifyMounted();One capability, over whatever is already in force, put back on dispose. Nesting composes. The
ceremony it replaces was a resolver written by hand, which answered null to every OTHER capability
the component might ask for and had to be undone in a finally that a test could forget.
Since 0.2.0-preview.13
The same page has to be server-renderable, and the server has no camera. Every capability resolves there to an ABSENT realization: it reports itself unavailable and hands back nothing.
Without it the page could not be constructed at all: no constructor the container could satisfy, and the request ended in a 500. The one page that does something was the one page a crawler never saw, and the visitor waited for JavaScript just to be told the page exists.
Absent, deliberately, and not simulated. There is no camera in a datacenter and the visitor's
localStorage is on the visitor's machine, so a server-side fake would be worse than the failure:
the page would render one thing, the browser would hydrate another, and the mismatch would be blamed
on the reconciler. The page takes the availability branch it already has to have, and the client
boot's real capability replaces the fallback on the first client render.
INetworkStatus is the exception, and the exception is the point: reporting offline would bake the
offline banner into the markup every crawler and every first paint sees, for visitors who just
proved they are online by fetching the page. It answers online.
Registered with
TryAdd, so an app with a genuine server-side answer (a storage backed by the user's session) registers its own and wins.
🌐 English · Português
🏁 Start here
🏗️ Architecture
- Architecture Overview
- Write-Once Components
- Declarative Surface
- Package Architecture
- Components
- Styling
- Localization
- Analytics & GTM
📱 Write-once
⚙️ Compilation
⚡ Runtime
🔌 Server
🎨 Ecosystem
🚀 Development