Live Screens¶
A screen is normally drawn once and then waits for the user. ConsoleMVC has two ways to keep it moving:
- Live regions redraw a
<progress>bar or a<spinner>in place while an action works. - Timed refresh draws a view again every few seconds, with fresh data, until the user starts typing.
Progress while an action works¶
An action runs before its view is drawn, so a long one leaves the user looking at the previous screen. Show what it is
doing with ILiveDisplay, which the framework registers for you:
public class ImportController(ILiveDisplay live, IImporter importer) : Controller
{
public async Task<ActionResult> Run(CancellationToken cancellationToken)
{
var progress = new ProgressElement { Label = "Import ", Max = importer.Count };
await using (var region = live.Start(progress))
{
await foreach (var _ in importer.ImportAsync(cancellationToken))
region.Update(() => progress.Value++);
}
return View();
}
}
Select an option: 1
Import ██████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 40%
Startdraws the element, andUpdatechanges it and redraws it in the same step.- The region also redraws on a timer (every 100 ms; pass another interval to
Start), which is what turns spinners. Changes made withoutUpdateshow at the next tick, or straight away withregion.Refresh(). - Disposing the region draws the element one last time. It stays on the screen until the next view clears it.
- Draw nothing else while a region is live: the region replaces the last element drawn.
- When the output is a pipe or a file, lines cannot be redrawn, so the element is drawn once, in its final state, when
the region ends. Logs then show
Import ████████ 100%rather than every step.
Any element can be live, including a <box> with several bars in it. A spinner shows that something is happening when
there is nothing to count:
var spinner = new SpinnerElement { Label = "Connecting" };
await using (var region = live.Start(spinner))
{
await client.ConnectAsync(cancellationToken);
region.Update(() => { spinner.Label = "Connected"; spinner.Done = true; }); // ✓ Connected
}
Spinners, and any element of your own that implements IAnimatedElement, move to their next frame at each tick, also
inside containers.
Refreshing a view on a timer¶
A dashboard should show fresh numbers without waiting for a key. Add @refresh to the view:
@model MyApp.Models.StatusViewModel
@refresh 5s
<text>Queue length: @Model.QueueLength</text>
<progress label="Disk " value="@Model.DiskUsed" max="@Model.DiskSize" />
<menu>
<option key="b" back>Back</option>
</menu>
Every 5 seconds the action runs again (so it reads fresh data) and the screen is cleared and drawn again.
- Refresh stops while the user answers. As soon as a key is pressed on the screen, it waits for the user like any other.
- The interval is a number with
ms,sorm(500ms,5s,1.5s,2m), from 100 ms to 24 hours. Anything else is build error CVW010. - Only views refresh: a partial view or a layout with
@refreshis CVW010 too. - A refresh shows the same screen again, so it does not add to the navigation history: Back still goes to the screen before.
- When input is piped (not a terminal), the application cannot tell whether the user is typing, so screens do not refresh.
The action can choose instead, and it wins over the view's @refresh:
public ActionResult Status() => View(LoadStatus()).RefreshEvery(TimeSpan.FromSeconds(2));
public ActionResult Report() => View(LoadReport()).NoRefresh(); // the view's @refresh is turned off
To make a spinner turn on a refreshing screen, give it a frame that changes, for example frame="@DateTime.Now.Second".
Testing¶
ScriptedInput.Wait() is a pause in which the user types nothing: a refreshing screen uses it up and refreshes.
var run = host.Run(new ScriptedInput("4").Wait().Wait().Line("q"));
Assert.Equal(["Home/Index", "Status/Index", "Status/Index", "Status/Index"], run.Routes);
A live region in an action draws into the test's screens like it would on a terminal, so run.Output holds the final
state of the bar. See Testing.
Your own input¶
The framework asks the input whether the user has started typing through ITextInput.WaitForInput(timeout). The
default implementation returns true straight away (go ahead and read), so an input that does not implement it never
refreshes a screen. Return false when the time passes with nothing typed.