Async data and HTTP
Load data without blocking the UI: results come back on the UI thread.
Load data with a Loadable<T> and draw each state. Loads run off the UI thread; their results, and every
await in RunTask, come back on it.
var services = new Loadable<IReadOnlyList<ServiceDto>>();
var selection = new SelectionState();
var toasts = new ToastQueue();
var app = App.Create();
var keys = new KeyMap()
.Add("r", "Refresh", () => services.Load(app, ct => api.ListAsync(ct))) // cancels a load in flight
.Add("j,down", "Down", () => selection.Next(services.Value?.Count ?? 0))
.Add("k,up", "Up", () => selection.Previous(services.Value?.Count ?? 0))
.Add("q,ctrl+c", "Quit", () => app.Quit());
app.OnSetup(a => services.Load(a, ct => api.ListAsync(ct)))
.OnKey(keys.Handle)
.OnError(ex => { toasts.Push(ex.Message, ToastLevel.Error); return ErrorAction.Continue; })
.Render((buf, screen) =>
{
var layout = Layout.Vertical(screen).Add("body", Constraint.Fill()).Add("status", Constraint.Length(1)).Build();
services.Switch(
idle: () => Widget.Paragraph().Text("Press r to load").Draw(buf, layout["body"]),
loading: stale => DrawTable(buf, layout["body"], stale), // the previous value, if any
loaded: list => DrawTable(buf, layout["body"], list),
failed: ex => Widget.Paragraph().Text(new Line(ex.Message, Theme.Current.Error)).Wrap(WrapMode.Word)
.Block(Widget.Block().Title(" Error ")).Draw(buf, layout["body"]));
Widget.StatusBar().Left(keys.Hints(screen.Width / 2)).Draw(buf, layout["status"]);
Widget.Toasts().Queue(toasts).Draw(buf, screen);
})
.Run();
void DrawTable(Buffer buf, Rect area, IReadOnlyList<ServiceDto>? list) =>
Widget.Table()
.Block(Widget.Block().Title(" Services "))
.Header("Name", "Status", "Version")
.Columns(Constraint.Fill(), Constraint.Length(10), Constraint.Length(12))
.Rows(list ?? [], s => new Row(s.Name, new Line(s.Status, s.Healthy ? Theme.Current.Success : Theme.Current.Error), s.Version))
.State(selection)
.Focused()
.Draw(buf, area);
Loadable<T> holds Status (Idle, Loading, Loaded, Failed), the last Value (kept while
reloading and after a failure), Error and LoadedAt. State changes happen on the UI thread and redraw.
A newer load replaces an older one, and a failed fetch sets Failed instead of going to OnError. Pass a
lifetime token (or use Component.Load) to ignore results once their owner is gone.