Goal
Why this matters
Classes, PseudoClasses, Transitions, Interaction) that work perfectly in C# once you know where to look.Prerequisites
Avalonia’s property system already supports observables. AvaloniaObject exposes GetObservable and GetPropertyChangedObservable so you can build reactive pipelines without XAML triggers.
var textBox = new TextBox();
textBox.GetObservable(TextBox.TextProperty)
.Throttle(TimeSpan.FromMilliseconds(250), RxApp.MainThreadScheduler)
.DistinctUntilChanged()
.Subscribe(text => _search.Execute(text));
Use ObserveOn(RxApp.MainThreadScheduler) to marshal onto the UI thread when subscribing. For non-ReactiveUI projects, use DispatcherScheduler.Current (from Avalonia.Reactive) or Dispatcher.UIThread.InvokeAsync inside the observer.
ReactiveUI view-models usually expose ReactiveCommand and ObservableAsPropertyHelper. Bind them as usual, but you can also subscribe directly:
var vm = new DashboardViewModel();
vm.WhenAnyValue(x => x.IsLoading)
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(isLoading => spinner.IsVisible = isLoading);
WhenAnyValue is extension from ReactiveUI. For code-first views, you may bridge them via constructor injection, ensuring the view wires observable pipelines in its constructor or OnAttachedToVisualTree lifecycle methods.
DynamicData shines when projecting observable collections into UI-friendly lists.
var source = new SourceList<ItemViewModel>();
var bindingList = source.Connect()
.Filter(item => item.IsEnabled)
.Sort(SortExpressionComparer<ItemViewModel>.Descending(x => x.CreatedAt))
.ObserveOn(RxApp.MainThreadScheduler)
.Bind(out var items)
.Subscribe();
listBox.Items = items;
Dispose the subscription when the control unloads to prevent leaks (e.g., store IDisposable and dispose in DetachedFromVisualTree).
Classes and PseudoClassesClasses and PseudoClasses collections (defined in Avalonia.Styling) let you toggle CSS-like states entirely from C#.
var panel = new Border();
panel.Classes.Add("card"); // corresponds to :class selectors in styles
panel.PseudoClasses.Set(":active", true);
Use helpers to line up state changes with view-model events:
vm.WhenAnyValue(x => x.IsSelected)
.Subscribe(selected => panel.Classes.Toggle("selected", selected));
Toggle is an extension you can write:
public static class ClassExtensions
{
public static void Toggle(this Classes classes, string name, bool add)
{
if (add)
classes.Add(name);
else
classes.Remove(name);
}
}
Avalonia.InteractivityInteraction (in external/Avalonia/src/Avalonia.Interactivity/Interaction.cs) provides behaviour collections similar to WPF. You can attach behaviours programmatically via Interaction.SetBehaviors.
Interaction.SetBehaviors(listBox, new BehaviorCollection
{
new SelectOnPointerOverBehavior()
});
Behaviours are regular classes implementing IBehavior. Author your own to encapsulate complex logic like drag-to-reorder.
Transitions collection (from Avalonia.Animation) lives on Control. Build transitions and hook them dynamically.
panel.Transitions = new Transitions
{
new DoubleTransition
{
Property = Border.OpacityProperty,
Duration = TimeSpan.FromMilliseconds(200),
Easing = new CubicEaseOut()
}
};
Activate transitions via property setters:
vm.WhenAnyValue(x => x.ShowDetails)
.Subscribe(show => panel.Opacity = show ? 1 : 0);
The change triggers the transition. Because transitions live on the control, you can swap them per theme or feature by replacing the Transitions collection at runtime.
Animatable.BeginAnimation (from AnimationExtensions) lets you trigger storyboards without styles:
panel.BeginAnimation(Border.OpacityProperty, new Animation
{
Duration = TimeSpan.FromMilliseconds(400),
Easing = new SineEaseInOut(),
Children =
{
new KeyFrames
{
new KeyFrame { Cue = new Cue(0d), Setters = { new Setter(Border.OpacityProperty, 0d) } },
new KeyFrame { Cue = new Cue(1d), Setters = { new Setter(Border.OpacityProperty, 1d) } }
}
}
});
Encapsulate animations into factory methods for reuse across views.
While Avalonia’s XAML Previewer focuses on markup, code-first workflows can approximate hot reload using:
DevTools: AttachDevTools() on the main window or AppBuilder (see ApplicationLifetimes).Avalonia.ReactiveUI HotReload packages or community tooling for reloading compiled assemblies.Enable DevTools programmatically in debug builds:
if (Debugger.IsAttached)
{
this.AttachDevTools();
}
For headless tests, log control trees after creation to confirm state without UI.
Integrate logging by observing key properties and commands.
var subscription = panel.GetPropertyChangedObservable(Border.OpacityProperty)
.Subscribe(args => _logger.Debug("Opacity changed from {Old} to {New}", args.OldValue, args.NewValue));
Tie into Avalonia’s diagnostics overlays (Chapter 24) by enabling them in code-first startup:
if (Debugger.IsAttached)
{
RenderOptions.ProcessRenderOperations = true;
RendererDiagnostics.DebugOverlays = RendererDebugOverlays.Fps | RendererDebugOverlays.Layout;
}
Create a shared library of helpers tailored to your code-first patterns:
public static class ReactiveControlHelpers
{
public static IDisposable BindState<TViewModel>(this TViewModel vm, Control control,
Expression<Func<TViewModel, bool>> property, string pseudoClass)
{
return vm.WhenAnyValue(property)
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(value => control.PseudoClasses.Set(pseudoClass, value));
}
}
Use it in views:
_disposables.Add(vm.BindState(this, x => x.IsActive, ":active"));
Maintain a CompositeDisposable on the view to dispose subscriptions when the view unloads. Override OnAttachedToVisualTree/OnDetachedFromVisualTree to manage lifetime.
WhenAnyValue on a view-model and toggles Classes on a panel. Verify with headless tests that pseudo-class changes propagate to styles.Transitions configured per theme (e.g., fast vs. slow). Swap collections at runtime and instrument the effect with property observers.PointerMoved events into an observable stream. Use it to implement drag selection without code-behind duplication.Reactive helper patterns ensure code-first Avalonia apps stay expressive, maintainable, and observable. By leveraging observables, behaviours, transitions, and tooling APIs directly from C#, your team keeps the productivity of markup-driven workflows while embracing the flexibility of a single-language stack.
What's next