Tutorial: TODO App
Run the complete sample:
dotnet run --project samples/Todo/Todo.csproj
The sample does not use NullRenderHost. It requires WebKitGTK 6.0,
JavaScriptCoreGTK 6.0, GTK 4, and a working DISPLAY or WAYLAND_DISPLAY.
1. Configure CSX
<ProjectReference Include="..\..\src\UltimateUI.Components.SourceGen\UltimateUI.Components.SourceGen.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
<AdditionalFiles Include="TodoView.uui.csx" />
2. Write TodoView.uui.csx
TodoShell is the TODO showcase root component: it owns draft state, the todo
list, and event handlers internally, and expands the list directly in the CSX
children. TodoRow only owns one item row. A CSX file still uses normal C#
structure; only the return (<.../>); expression inside a method is extended.
The {todos.Value.Select(...)} child inserts the IEnumerable<Element> built
in C#.
using System;
using System.Collections.Generic;
using System.Linq;
using UltimateUI;
using UltimateUI.Components;
using UltimateUI.Events;
namespace UltimateUI.Samples.Todo;
public static partial class TodoView
{
public static Element TodoShell(ComponentContext context)
{
var draft = context.UseState("Read the docs");
var todos = context.UseState<IEnumerable<TodoItem>>(() =>
new[]
{
new TodoItem(1, "Build component runtime", Done: true),
new TodoItem(2, "Ship TODO example", Done: false),
});
EventHandler<InputEventArgs> onDraftInput = (_, e) => draft.Set(e.Value);
EventHandler<ClickEventArgs> onAdd = (_, _) =>
{
var title = draft.Value.Trim();
if (title.Length == 0)
{
return;
}
var nextId = todos.Value.Any() ? todos.Value.Max(static item => item.Id) + 1 : 1;
todos.Set(todos.Value.Concat(new[] { new TodoItem(nextId, title, Done: false) }));
draft.Set(string.Empty);
};
var openCount = todos.Value.Count(static item => !item.Done);
return (
<div className="todo-shell">
<h1>TODO</h1>
<p>Open items: {openCount}</p>
<div className="todo-row">
<input className="todo-input" placeholder="New task" value={draft.Value} onInput={onDraftInput} />
<button className="todo-button" onClick={onAdd}>Add</button>
</div>
{todos.Value.Select(item => TodoRow(
context,
item.Id,
item.Title,
item.Done,
(_, _) => todos.Set(todos.Value
.Select(candidate => candidate.Id == item.Id ? candidate with { Done = !candidate.Done } : candidate))))}
</div>
);
}
public static Element TodoRow(ComponentContext context, int id, string title, bool done, EventHandler<ClickEventArgs>? onToggle)
{
return (
<div className="todo-row" key={id}>
<button className="todo-button" onClick={onToggle}>{done ? "Undo" : "Done"}</button>
<span className={done ? "todo-title todo-done" : "todo-title"}>{title}</span>
</div>
);
}
}
TodoItem is a normal C# record in the same namespace. It can live at the end
of Program.cs or in a separate .cs file:
internal readonly record struct TodoItem(int Id, string Title, bool Done);
3. Manage state inside TodoShell
var draft = context.UseState("Read the docs");
var todos = context.UseState<IEnumerable<TodoItem>>(() =>
new[]
{
new TodoItem(1, "Build component runtime", Done: true),
new TodoItem(2, "Ship TODO example", Done: false),
});
4. Bind events and inline list rendering
EventHandler<InputEventArgs> onDraftInput = (_, e) => draft.Set(e.Value);
EventHandler<ClickEventArgs> onAdd = (_, _) => { /* append todo */ };
{todos.Value.Select(item => TodoRow(
context,
item.Id,
item.Title,
item.Done,
(_, _) => { /* toggle todo */ }))}
5. Mount on WebKitGTK
var builder = UltimateAppBuilder.CreateDefaultBuilder();
var app = builder.Build();
var window = app.MainWindow = new Window();
window.AttachGlobalStyle(styles);
window.Render(TodoView.TodoShell);
return await app.RunAsync();