Skip to main content

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();