The Reactive MVU Pattern
Model-Update-Observable
In a standard MVU loop the View function runs on every Model change and produces a new virtual tree, which is then diffed against the previous one. MVUish.Avalonia keeps the Model and Update halves of Elm but replaces the diffing view with a direct binding:
- Model and Update stay pure and immutable.
Updateprocesses aMsgand returns the newModelplus a command. - View runs once at startup. It receives an
IObservable<Model>and adispatchfunction. - Bindings carry the updates. Each control property subscribes to the slice of the stream it needs, so only the properties that change are updated.
There is no tree diffing and no wrapper controls: the view returns real Avalonia controls, so third-party controls and layouts work without special support.
Programs
Program.mkProgram (F#) / Program.MkProgram (C#) takes init, update, and view. The builder functions add behavior:
F# |
C# |
Purpose |
|---|---|---|
|
|
Register the keyed subscription set; re-evaluated per update |
|
— |
Accumulate instead of replace subscriptions |
|
|
Exit message plus final-model handler |
|
|
Trace message, model, and wanted sub ids |
|
|
Log messages, models, and active sub ids |
|
|
Handle command and update errors |
|
|
Start the loop and get the view |
|
|
View plus an |
|
|
Program tied to a control's visual-tree lifetime |
Update and dispatch are marshalled to the UI thread, and dispatch is thread-safe from any thread.
Commands
A Cmd<'Msg> (C# Action<Action<'Msg>, Action<string, exn>>) runs side effects and dispatches messages. Program routes errors from commands to the error handler with the message that caused them.
Common helpers:
F# |
C# |
Purpose |
|---|---|---|
|
|
No side effect |
|
|
Dispatch a message immediately |
|
— |
Effect that receives |
|
|
Plain |
|
|
Run several commands |
|
|
Remap a child command for composition |
|
|
Run a sync function, dispatch the result |
|
|
Run a sync function, dispatch the error |
|
|
Run a sync function, map success or error to messages |
|
— |
Run an |
|
— |
Same, with a custom async start |
|
|
Run a |
|
|
Run a |
|
|
Native ValueTask commands |
Subscriptions
A Sub<'Msg> (C# CSub<Msg>, built with the Subscription helpers) is the set of external event sources the current model wants active. The program re-evaluates the set after every update and starts or stops subscriptions to match; see Subscriptions for the full guide.
F# |
C# |
Purpose |
|---|---|---|
|
|
No subscriptions |
|
|
Combine several subscription sets |
|
|
Scope child ids and translate child messages |
|
|
Dispatch a tick message on a timer |
|
|
Bridge a .NET event into messages |
|
|
Bridge any |
|
|
Bridge an Avalonia routed event |
|
|
End the set when a control detaches |
Lifecycle
Programs own resources: subscriptions, the update loop, and the model stream. runWithLifecycle returns the handle that releases all three; runWithLifetime ties that release to a control leaving the visual tree; withTermination makes a message do it. Completing the model stream ends every subscription derived from it and reverts bound properties to their defaults — details in Subscriptions.
Composition and scaling
Modules compose like standard Elmish modules. A parent model holds the child models, and the parent maps child commands, subscriptions, and dispatchers:
Command.Map/Cmd.maproutes child commands upward.Subscription.Map/Sub.maproutes child subscriptions upward, scoping their ids with a prefix.Subscription.Batch/Sub.batchcombines subscription sets.
In the view, slice the parent stream with Observable.map / Select and hand each child its own observable and dispatch function:
let view (model: IObservable<Model>) (dispatch: Dispatch<Msg>) =
let clockObs = model |> Observable.map(fun m -> m.Clock)
let dispatchClock = ClockMsg >> dispatch
StackPanel().Children(
Clock.view clockObs dispatchClock
)
The dashboard sample wires three child modules this way, in both languages.
MVUish.Avalonia