Table of Contents

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.

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:

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 transform and opacity, 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