Periodic processing in client code
Client C# code (UIView and UIComponent methods, event rules) can run a processing at a regular interval, for example refreshing data every 30 seconds or rotating tiles every 10 seconds.
Warning
A periodic processing changes a state at a low frequency, with a period of one second or more. Never use it to
animate: transitions, blinking, rotations or waiting indicators are run by the browser, with the template
transition: directives, the native components or an animated SVG image. Always pass LifetimeToken to every wait,
and never loop on Task.Delay without a token. See Usages to avoid.
Recommended pattern
Write the loop in an asynchronous UIView method and pass LifetimeToken to every wait (the only exception, a wait on
another token, is described in Canceling a wait within one method call):
using PeriodicTimer timer = new(TimeSpan.FromSeconds(10));
while (await timer.WaitForNextTickAsync(LifetimeToken))
{
Fields.FlippedTileIndex = (Fields.FlippedTileIndex + 1) % Fields.TileCount;
}
Start it from the Initialized rule without awaiting it:
_ = Methods.RotateTilesAsync();
The Initialized rule is awaited before the screen finishes opening: awaiting an endless loop there would block it.
- A tick starts only once the previous one is processed: two ticks never overlap.
- Ticks are aligned on the clock. When a processing lasts longer than the period, the missed ticks are merged into one, which completes immediately.
Animating the change
The timer decides when the state changes, the template decides how the change is displayed. The loop above only changes the index of the displayed tile, once every 10 seconds. The visual switch, for example a fade, is animated by the browser with the transition directives of the template:
<vertical-layout
style:opacity="@Computeds.FirstTileOpacity"
transition:properties="opacity"
transition:duration="400"
transition:easing="ease-in-out"
>
...
</vertical-layout>
Content that changes by itself, such as rotating tiles, must offer a way to pause it, for example a Stop action (see Stopping on demand): users must be able to read it at their own pace.
Pausing while the page is hidden
The timer keeps ticking while the browser tab is in the background or the window is minimized, but the browser does not render the page and plays no transition there. When the page comes back, every change made in the meantime is displayed at once: several tiles turn together, for example, instead of one after the other.
When a tick only changes what is displayed, skip it while the page is hidden with Window.Current.IsPageVisible:
using PeriodicTimer timer = new(TimeSpan.FromSeconds(10));
while (await timer.WaitForNextTickAsync(LifetimeToken))
{
if (!Window.Current.IsPageVisible)
{
continue;
}
Fields.FlippedTileIndex = (Fields.FlippedTileIndex + 1) % Fields.TileCount;
}
The skipped ticks are not caught up: the rotation resumes at the next tick once the page is visible again.
Window.Current.IsPageVisible is not reactive: read it in the code that needs it, as above, never bind it in a
template or a computed.
Usages to avoid
Do not animate with a timer
A periodic processing changes a state at a low frequency: refreshing data, moving to the next tile, checking whether an export is ready. It must never draw an animation step by step:
// Never do this: an animation driven by the timer
using PeriodicTimer timer = new(TimeSpan.FromMilliseconds(50));
while (await timer.WaitForNextTickAsync(LifetimeToken))
{
Fields.TileRotationAngle = (Fields.TileRotationAngle + 10) % 360;
}
Each tick changes a field of the model, which updates the screen and runs the transpiled code on the main thread of the browser, along with everything else the screen does. The animation is jerky, slows the whole screen down, and drifts as soon as the browser is busy or the tab is hidden. An animation run by the browser itself is smooth, does not touch the model and costs almost nothing.
Use instead:
- the transition directives to animate a change of state, as in Animating the change
- the native components for the standard waiting indicators, such as the progress bar or the waiting overlay
- an animated SVG image for a decorative animation that runs continuously (see below)
Keep the period of a periodic processing at one second or more. A shorter period is almost always an animation in disguise, or a polling of the server that is far too frequent.
Animated SVG images
A continuous decorative animation, such as a pulsing icon or a rotating logo, can be written inside an SVG file of the
module images folder (see Images), with CSS animations (@keyframes) or SVG animation elements
(<animate>, <animateTransform>). The browser plays it on its own, with no C# code and no change of the model, so it
has no impact on the screen logic.
To keep it light:
- animate
transformandopacity, which the browser handles efficiently, rather than the shape of paths or their colors - avoid animated filters such as blur or shadows, and animations on a large number of elements
- stop or simplify the animation for users who reduce motion, with
@media (prefers-reduced-motion: reduce)in the CSS of the SVG - do not rely on scripts inside the SVG: they are not run when the SVG is displayed as an image
How the SVG is displayed depends on the Colored property of the image metadata, not on the colors of the file:
with Colored: true (the default), the SVG is displayed as is, as a background image; with Colored: false, it is
used as a mask colored with the current text color. Check the rendering of the animation in the browsers you target,
in particular for an image declared with Colored: false.
Do not loop on Task.Delay without a token
// Never do this
while (true)
{
await Task.Delay(10000);
await Methods.RefreshAsync();
}
Such a loop never stops: it keeps the closed screen's model in memory, keeps changing its fields or calling the server, and a new loop starts at each reopening of the screen.
Lifetime token
LifetimeToken is available on UIView and UIComponent models. It is canceled when the screen is closed or the
UIComponent destroyed, before the Disposed rule runs. A loop or a Task.Delay using it stops immediately, even
while waiting. This stop is never displayed nor logged as an error. No code is needed in the Disposed rule.
Stopping on demand
A loop can wait on only one token, and linked token sources are not supported. The loop therefore waits on
LifetimeToken, so that closing the screen stops it immediately, and a Stop action asks it to stop through a boolean
field of the view, checked by the loop body:
using PeriodicTimer timer = new(TimeSpan.FromSeconds(1));
while (await timer.WaitForNextTickAsync(LifetimeToken))
{
if (Fields.Stopped)
{
break;
}
Fields.TickCount++;
}
// Stop action
Fields.Stopped = true;
The loop ends at the next tick, not immediately, because it is only waiting on the timer and the lifetime token. To
start it again, reset the field to false before calling the method.
Canceling a wait within one method call
Use a CancellationTokenSource to cancel a wait scoped to one method call, for example to give up polling when a
condition evaluated in the same method is met:
using CancellationTokenSource source = new();
int attemptCount = 0;
try
{
while (!await Methods.IsExportReadyAsync())
{
attemptCount++;
if (attemptCount == 10)
{
source.Cancel();
}
await Task.Delay(TimeSpan.FromSeconds(2), source.Token);
// The wait does not use LifetimeToken: stop here if the screen was closed meanwhile.
LifetimeToken.ThrowIfCancellationRequested();
}
Fields.Status = "Export ready";
}
catch (OperationCanceledException)
{
Fields.Status = "Export not ready yet, try again later";
}
A wait on another token than LifetimeToken is the only exception to the rule of passing LifetimeToken to every
wait. Such a wait does not stop when the screen is closed: check LifetimeToken right after each await, with
LifetimeToken.ThrowIfCancellationRequested();, so that closing the screen stops the method within one iteration
instead of letting it keep calling the server. The exception thrown is caught by the
catch (OperationCanceledException) above, which then only updates a field of the closed screen.
A canceled wait throws an OperationCanceledException, or a TaskCanceledException for Task.Delay. Both can be
caught with catch (OperationCanceledException), and catch (TaskCanceledException) catches only the second one.
When not caught, they are silently ignored by the framework.
Limitations
| Topic | Behavior |
|---|---|
| Supported members | PeriodicTimer, CancellationTokenSource (Token, Cancel, Dispose), CancellationToken (IsCancellationRequested, ThrowIfCancellationRequested, None), OperationCanceledException, TaskCanceledException, Task.Delay with a TimeSpan or a token, TimeSpan.FromSeconds and TimeSpan.FromMinutes |
| Not supported | CancelAfter, linked token sources, CancellationToken.Register, await using |
default literal |
Not supported: write CancellationToken.None instead of default |
| Canceled source | A canceled CancellationTokenSource cannot be reused: create a new one. In the browser, Dispose releases nothing, it only exists for parity with .NET |
| Long durations | PeriodicTimer and Task.Delay reject a duration above about 24.8 days (2 147 483 647 ms), the maximum of the browser timers |
| Infinite wait | Task.Delay(-1, token) waits until the token is canceled (write -1: Timeout.Infinite is not transpiled) |
| Background browser tabs | Browsers slow down the timers of hidden tabs, so ticks may be delayed. To skip ticks while the page is hidden, see Pausing while the page is hidden |