xaml-csharp-development-skill-for-avalonia

Reactive Patterns and UI Threading

Objective

Keep state propagation reactive while guaranteeing UI-thread correctness.

UI Thread Contract

All visual tree and control property updates must run on Dispatcher.UIThread.

Primary APIs:

Use:

Dispatcher Operation Lifecycle

InvokeAsync(...) returns DispatcherOperation / DispatcherOperation<T>.

High-value APIs:

Pattern:

var op = Dispatcher.UIThread.InvokeAsync(
    () => ApplyUiDelta(snapshot),
    DispatcherPriority.Background);

if (cancellationToken.IsCancellationRequested)
    op.Abort();

await op.GetTask();

Guideline:

Priority Strategy

Useful priorities in app code:

Keep priority choices stable and intentional. Escalating everything to high priority causes responsiveness regressions.

Await With Priority

Dispatcher.UIThread.AwaitWithPriority(...) lets you continue on the dispatcher with explicit priority after a task completes.

await Dispatcher.UIThread.AwaitWithPriority(loadTask, DispatcherPriority.Background);
// Continuation is now queued on UI dispatcher at Background priority.
UpdateUiFromLoadedData();

Use this when you need deterministic continuation priority instead of default scheduler behavior.

DispatcherTimer Patterns

For scheduled UI work:

IDisposable heartbeat = DispatcherTimer.Run(
    action: () =>
    {
        UpdateClockText();
        return !viewModel.IsDisposed;
    },
    interval: TimeSpan.FromSeconds(1),
    priority: DispatcherPriority.Background);

Dispose returned timer handles during teardown to avoid stale callbacks.

Dispatcher Exception Pipeline

Dispatcher exposes exception hooks for invoked delegates:

Use them as a final safety net and telemetry path, not as normal control flow.

Reactive Property Pipelines

Useful entry points:

Example: bind observable stream to UI property

IDisposable subscription = textBlock.Bind(
    TextBlock.TextProperty,
    viewModel.StatusObservable);

Example: observe property changes

IDisposable subscription = textBox
    .GetObservable(TextBox.TextProperty)
    .Subscribe(value =>
    {
        // Transform/validate or dispatch commands.
    });

Command and Input Flow

Command/input APIs:

Routing APIs:

Use:

Thread-Safe Async Pattern

public async Task RefreshAsync()
{
    IsBusy = true;
    try
    {
        var dto = await _api.GetDataAsync();

        await Dispatcher.UIThread.InvokeAsync(() =>
        {
            Items.Clear();
            foreach (var item in dto.Items)
                Items.Add(item);
        });
    }
    finally
    {
        IsBusy = false;
    }
}

Avoiding Reactive Pitfalls

  1. Updating control properties from background threads.
    • Fix: marshal to Dispatcher.UIThread.
  2. Manual event subscriptions without deterministic disposal.
    • Fix: keep and dispose subscriptions with view/lifetime ownership.
  3. Coupling viewmodels directly to concrete control instances.
    • Fix: expose state/commands only; let bindings wire controls.
  4. Using heavy sync work inside Dispatcher.Invoke.
    • Fix: compute off-thread, then apply minimal UI delta on UI thread.

Practical Disposal Guidance

XAML-First and Code-Only Usage

Default mode:

XAML-first references:

XAML-first usage example:

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:vm="using:MyApp.ViewModels"
             x:Class="MyApp.Views.StatusView"
             x:DataType="vm:StatusViewModel">
  <StackPanel Spacing="8">
    <TextBlock Text="{CompiledBinding StatusText}" />
    <ProgressBar IsIndeterminate="{CompiledBinding IsBusy}" />
  </StackPanel>
</UserControl>

Code-only alternative (on request):

textBlock.Bind(TextBlock.TextProperty, viewModel.StatusObservable);
progressBar.Bind(ProgressBar.IsIndeterminateProperty, viewModel.IsBusyObservable);