Goal
TopLevel services (clipboard, storage, screens) from view models via abstractions.Why this matters
Prerequisites
| Lifetime | Use case | Entry method |
|---|---|---|
ClassicDesktopStyleApplicationLifetime |
Windows/macOS/Linux windowed apps | StartWithClassicDesktopLifetime(args) |
SingleViewApplicationLifetime |
Mobile (Android/iOS), embedded | StartWithSingleViewLifetime(view) |
BrowserSingleViewLifetime |
WebAssembly | BrowserAppBuilder setup |
ISingleTopLevelApplicationLifetime |
Single top-level host (preview/embedded scenarios) | Exposed by the runtime; inspect via ApplicationLifetime as ISingleTopLevelApplicationLifetime |
App.OnFrameworkInitializationCompleted should handle all lifetimes:
public override void OnFrameworkInitializationCompleted()
{
var services = ConfigureServices();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
var shell = services.GetRequiredService<MainWindow>();
desktop.MainWindow = shell;
// optional: intercept shutdown
desktop.ShutdownMode = ShutdownMode.OnLastWindowClose;
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
{
singleView.MainView = services.GetRequiredService<ShellView>();
}
base.OnFrameworkInitializationCompleted();
}
ISingleTopLevelApplicationLifetime is currently marked [PrivateApi], but you may see it when Avalonia hosts supply a single TopLevel. Treat it as read-only metadata rather than something you implement yourself.
When targeting browser, use BrowserAppBuilder with SetupBrowserApp.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Opened += (_, _) => RestorePlacement();
Closing += (_, e) => SavePlacement();
}
private const string PlacementKey = "MainWindowPlacement";
private void RestorePlacement()
{
if (LocalSettings.TryReadWindowPlacement(PlacementKey, out var placement))
{
Position = placement.Position;
Width = placement.Size.Width;
Height = placement.Size.Height;
}
}
private void SavePlacement()
{
LocalSettings.WriteWindowPlacement(PlacementKey, new WindowPlacement
{
Position = Position,
Size = new Size(Width, Height)
});
}
}
LocalSettings is a simple persistence helper (file or user settings). Persisting placement keeps UX consistent.
public sealed class AboutWindow : Window
{
public AboutWindow()
{
Title = "About";
Width = 360;
Height = 200;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
Content = new TextBlock { Margin = new Thickness(16), Text = "My App v1.0" };
}
}
// From main window or service
public Task ShowAboutDialogAsync(Window owner)
=> new AboutWindow { Owner = owner }.ShowDialog(owner);
Modeless window:
var tool = new ToolWindow { Owner = this };
tool.Show();
Always set Owner so modal blocks correctly and centering works.
Use Screens service from TopLevel:
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel?.Screens is { } screens)
{
var screen = screens.ScreenFromPoint(Position);
var workingArea = screen.WorkingArea;
Position = new PixelPoint(workingArea.X, workingArea.Y);
}
Screens live under Avalonia.Controls/Screens.cs.
Subscribe to screens.Changed when you need to react to hot-plugging monitors or DPI changes:
screens.Changed += (_, _) =>
{
var active = screens.ScreenFromWindow(this);
Logger.LogInformation("Monitor layout changed. Active screen: {Bounds}", active.WorkingArea);
};
WindowBase.Screens always maps to the platform's latest monitor topology, so you can reposition tool windows or popups when displays change.
Closing += async (sender, e) =>
{
if (DataContext is ShellViewModel vm && vm.HasUnsavedChanges)
{
var confirm = await MessageBox.ShowAsync(this, "Unsaved changes", "Exit without saving?", MessageBoxButtons.YesNo);
if (!confirm)
e.Cancel = true;
}
};
Implement MessageBox yourself or using Avalonia.MessageBox community package.
WindowBase)WindowBase is the shared base type for Window and other top-levels. It raises events that fire before layout runs, letting you respond to activation, resizing, and positioning at the window layer:
public partial class ToolWindow : Window
{
public ToolWindow()
{
InitializeComponent();
Activated += (_, _) => StatusBar.Text = "Active";
Deactivated += (_, _) => StatusBar.Text = "Inactive";
PositionChanged += (_, e) => Logger.LogInformation("Moved to {Point}", e.Point);
Resized += (_, e) => Metrics.Track(e.Size, e.Reason);
Closed += (_, _) => _subscriptions.Dispose();
}
}
WindowBase.Resized reports the reason the platform resized your window (user drag, system DPI change, maximize). Distinguish it from Control.SizeChanged, which fires after layout completes. Use WindowBase.IsActive to trigger focus-sensitive behaviour such as pausing animations when the window moves to the background.
Avalonia exposes chrome customisation through TopLevel properties:
TransparencyLevelHint = new[] { WindowTransparencyLevel.Mica, WindowTransparencyLevel.Acrylic, WindowTransparencyLevel.Transparent };
SystemDecorations = SystemDecorations.None;
ExtendClientAreaToDecorationsHint = true;
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.SystemChrome | ExtendClientAreaChromeHints.OSXIssueUglyDropShadowHack;
WindowStartupLocation = WindowStartupLocation.CenterScreen;
Combine those settings with platform options to unlock OS-specific effects:
Win32PlatformOptions): enable CompositionBackdrop or UseWgl for specific GPU paths. Set WindowEffect = new MicaEffect(); to match Windows 11 styling.MacOSPlatformOptions): toggle ShowInDock, DisableDefaultApplicationMenu, and UseNativeMenuBar per window.X11PlatformOptions): control EnableIME, EnableTransparency, and DisableDecorations when providing custom chrome.Always test transparency fallbacks—older GPUs may fall back to Opaque. Query ActualTransparencyLevel at runtime to reflect final behaviour in the UI.
ShutdownRequestedEventArgsIClassicDesktopStyleApplicationLifetime exposes a ShutdownRequested event. Cancel it when critical work is in progress or when you must prompt the user:
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.ShutdownRequested += (_, e) =>
{
if (_documentStore.HasDirtyDocuments && !ConfirmShutdown())
e.Cancel = true;
if (e.IsOSShutdown)
Logger.LogWarning("OS initiated shutdown");
};
}
Return true from ConfirmShutdown() only after persisting state or when the user explicitly approves. Pair this with ShutdownMode to decide whether closing the main window exits the entire application.
public sealed class NavigationService : INavigationService
{
private readonly IServiceProvider _services;
private object? _current;
public object? Current
{
get => _current;
private set => _current = value;
}
public NavigationService(IServiceProvider services)
=> _services = services;
public void NavigateTo<TViewModel>() where TViewModel : class
=> Current = _services.GetRequiredService<TViewModel>();
}
ShellViewModel coordinates navigation:
public sealed class ShellViewModel : ObservableObject
{
private readonly INavigationService _navigationService;
public object? Current => _navigationService.Current;
public RelayCommand GoHome { get; }
public RelayCommand GoSettings { get; }
public ShellViewModel(INavigationService navigationService)
{
_navigationService = navigationService;
GoHome = new RelayCommand(_ => _navigationService.NavigateTo<HomeViewModel>());
GoSettings = new RelayCommand(_ => _navigationService.NavigateTo<SettingsViewModel>());
_navigationService.NavigateTo<HomeViewModel>();
}
}
Bind in view:
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8">
<Button Content="Home" Command="{Binding GoHome}"/>
<Button Content="Settings" Command="{Binding GoSettings}"/>
</StackPanel>
<TransitioningContentControl Content="{Binding Current}">
<TransitioningContentControl.Transitions>
<PageSlide Transition="{Transitions:Slide FromRight}" Duration="0:0:0.2"/>
</TransitioningContentControl.Transitions>
</TransitioningContentControl>
</DockPanel>
TransitioningContentControl (from Avalonia.Controls) adds page transitions. Source: TransitioningContentControl.cs.
Register view-model-to-view templates (Chapter 11 showed details). Example snippet:
<Application.DataTemplates>
<DataTemplate DataType="{x:Type vm:HomeViewModel}">
<views:HomeView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:SettingsViewModel}">
<views:SettingsView />
</DataTemplate>
</Application.DataTemplates>
For sidebars or hamburger menus, wrap the navigation service in a SplitView so content and commands share a host:
<SplitView IsPaneOpen="{Binding IsPaneOpen}"
DisplayMode="CompactOverlay"
CompactPaneLength="48"
OpenPaneLength="200">
<SplitView.Pane>
<ItemsControl ItemsSource="{Binding NavigationItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Title}"
Command="{Binding NavigateCommand}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</SplitView.Pane>
<TransitioningContentControl Content="{Binding Current}"/>
</SplitView>
Expose NavigationItems as view-model descriptors (title + command). Pair with SplitView.PanePlacement to adapt between desktop (left rail) and mobile (bottom sheet). Listen to TopLevel.BackRequested to collapse the pane when the host (Android, browser, web view) signals a system back gesture.
Expose a dialog API from view models without referencing Window:
public interface IDialogService
{
Task<bool> ShowConfirmationAsync(string title, string message);
}
public sealed class DialogService : IDialogService
{
private readonly Window _owner;
public DialogService(Window owner) => _owner = owner;
public async Task<bool> ShowConfirmationAsync(string title, string message)
{
var dialog = new ConfirmationWindow(title, message) { Owner = _owner };
return await dialog.ShowDialog<bool>(_owner);
}
}
Register a per-window dialog service in DI. For single-view scenarios, use TopLevel.GetTopLevel(control) to retrieve the root and use StorageProvider or custom dialogs.
For ISingleViewApplicationLifetime, use a root UserControl (e.g., ShellView) with the same TransitioningContentControl pattern. Keep navigation inside that control.
<UserControl xmlns="https://github.com/avaloniaui" x:Class="MyApp.Views.ShellView">
<TransitioningContentControl Content="{Binding Current}"/>
</UserControl>
From view models, use INavigationService as before; the lifetime determines whether a window or root view hosts the content.
TopLevel.GetTopLevel(control) returns the hosting top-level (Window or root). Useful for services.
var topLevel = TopLevel.GetTopLevel(control);
if (topLevel?.Clipboard is { } clipboard)
{
await clipboard.SetTextAsync("Copied text");
}
Clipboard API defined in IClipboard.
Works in both desktop and single-view (browser has OS limitations):
var topLevel = TopLevel.GetTopLevel(control);
if (topLevel?.StorageProvider is { } sp)
{
var file = (await sp.OpenFilePickerAsync(new FilePickerOpenOptions
{
AllowMultiple = false,
FileTypeFilter = new[] { FilePickerFileTypes.TextPlain }
})).FirstOrDefault();
}
topLevel!.Screens provides monitor layout. Use for placing dialogs on active monitor or respecting working area.
TopLevel.BackRequested bubbles up hardware or browser navigation gestures through Avalonia's ISystemNavigationManagerImpl. Subscribe to it when embedding in Android, browser, or platform WebView hosts:
var topLevel = TopLevel.GetTopLevel(control);
if (topLevel is { })
{
topLevel.BackRequested += (_, e) =>
{
if (_navigation.Pop())
e.Handled = true;
};
}
Mark the event as handled when your navigation stack consumes the back action; otherwise Avalonia lets the host perform its default behaviour (e.g., browser history navigation).
Use BrowserAppBuilder and BrowserSingleViewLifetime:
public static void Main(string[] args)
=> BuildAvaloniaApp().SetupBrowserApp("app");
Use TopLevel.StorageProvider for limited file access (via JavaScript APIs). Use JS interop for features missing from storage provider.
TopLevel.BackRequested maps to the browser's history stack—handle it to keep SPA navigation in sync with the host's back button.
WindowBase.Resized/PositionChanged, and persist placement per monitor.ShutdownRequested to prompt about unsaved documents, cancelling the shutdown when the user declines.Screens.Changed and reposition floating windows onto the active display when monitors are hot-plugged.SplitView navigation shell that collapses in response to TopLevel.BackRequested on Android or the browser.TransparencyLevelHint and SystemDecorations per platform and display the resulting ActualTransparencyLevel in the UI.Window.cs, WindowBase.csClassicDesktopStyleApplicationLifetime.cs, ShutdownRequestedEventArgs.csTopLevel.cs, SplitView.cs, SystemNavigationManagerImpl.csScreens.csTransitioningContentControl.csClassicDesktopStyleApplicationLifetime differ from SingleViewApplicationLifetime when showing windows?Show vs ShowDialog? Why set Owner?WindowBase events fire before layout, and how do they differ from SizeChanged?TopLevel.BackRequested improve the experience on Android or the browser?ShutdownRequestedEventArgs.IsOSShutdown tell you, and how would you react to it?TopLevel service would you use to access the clipboard or file picker from a view model?What's next