Skip to content

UI kit

Plugin dialogs and pages are Avalonia views with view models (CommunityToolkit.Mvvm). They use the host’s controls and theme, so a plugin looks and behaves like the rest of OADM in dark and light. Two rules cover most of it:

  • Reuse, never copy. Every element that exists in OADM is a shared control. Use it.
  • No local styles. No colors, font sizes, weights or paddings in your views. Use the theme’s classes and Oadm.*Brush keys; both appearances define every key.

Add the namespace xmlns:ui="using:Oadm.Sdk.Client.Controls" to your XAML.

Control Use for
ui:DialogTitleBar The first row of every dialog: the title and an X that closes it
ui:DialogFooter The last row: Cancel on the left, your buttons on the right
ui:CardHeader The heading of a card, with optional controls right of the title
ui:FormField Every labeled input: label, input, and the error or hint below it. Classes wide, inline; Info adds an info icon
ui:PasswordBox Every password field, with the eye button
ui:SearchBox Every search field
ui:ToolbarButton, ui:ToolbarSeparator Toolbar buttons (icon and text) and the line between groups
ui:IconLabel, ui:OadmIcon An icon with text (button content), a single icon
ui:StatusChip Every status: a colored icon (IsOk, IsWarning, IsError, IsAccent, IsInfo) and plain text
ui:StatCard A count card of a monitoring page
ui:ProgressRow A progress bar with its status text
ui:LoadingOverlay A dialog that waits for a read: a ring and text in the middle, Cancel stays usable
ui:FileRow A chosen file with its “Choose file” button
ui:CodeView, ui:CodeEditor Read-only code (JSON, XML, param.cgi answers, colored) and an editable code field
ui:InfoIcon An explanation that shows on hover and click
ui:WizardSteps The step row of a wizard
ui:DeviceIcon, ui:DeviceTree, ui:PageTab A device’s icon by category and status, devices grouped by tag, page tabs
MessageWindow Every message and confirmation: MessageWindow.ConfirmAsync(owner, title, message, confirmText)
DevicePickerWindow Every “choose devices” dialog: search, tags, Select all, 5,000 devices
ComboBox Classes="grouped" + GroupedChoice A select field with group headers

For lists that a grid shows, use RangeObservableCollection<T> (Oadm.Sdk.Client.Collections): ReplaceAll, AddRange and RemoveAll raise one change for the whole batch.

A dialog with one form has no card: title bar, the form in Border.dialogBody, footer. Fixed width, SizeToContent="Height":

<Window ... Width="500" SizeToContent="Height" CanResize="False"
WindowStartupLocation="CenterOwner" ShowInTaskbar="False" WindowDecorations="BorderOnly">
<Grid RowDefinitions="Auto,Auto,Auto">
<ui:DialogTitleBar Grid.Row="0" Text="Set UPnP name" />
<Border Grid.Row="1" Classes="dialogBody">
<StackPanel Classes="form">
<ui:FormField Label="Name">
<TextBox Text="{Binding Name}" />
</ui:FormField>
</StackPanel>
</Border>
<ui:DialogFooter Grid.Row="2" CancelCommand="{Binding CancelCommand}">
<Button Classes="primary" Content="OK" Command="{Binding ApplyCommand}"
ToolTip.Tip="{Binding FormError}" ToolTip.ShowOnDisabled="True" />
</ui:DialogFooter>
</Grid>
</Window>

A dialog with several sections or a list uses cards (Border.card, Margin="16,0") between title bar and footer, with a fixed width and height.

A page starts with its content. The description goes into the page header with ui:PageHeader.Subtitle, never into a card heading. A status sits directly left of the Save button it reports on.

Every validation error appears directly below its input: small red text and a red border. Never an error list at the bottom of a dialog. Form view models derive from ValidatingViewModel and register one rule per property:

public sealed partial class UserViewModel : ValidatingViewModel
{
public UserViewModel()
{
Validation
.Rule(nameof(UserName), () => UserName.Trim().Length == 0 ? "Enter a user name." : null)
.Rule(nameof(Confirm), () => Confirm != Password ? "The passwords do not match." : null);
Validation.Validate();
}
[ObservableProperty]
public partial string UserName { get; set; } = "";
// ...
}
  • An error shows once the field was edited or left, or the user tried to submit (Validation.ShowAll()); never on an untouched form. After loading values, Validation.Reset(...).
  • Submit buttons stay disabled while any error exists (IsFormValid), with the reason as tooltip (FormError).
  • An answer of the server that belongs to a field: Validation.SetServerError(property, message).
  • An error of a whole table goes directly below the table; an error of one row stays in its status column.
  • One accent button per view (Button.primary): the main action. Everything else is Button.secondary.
  • Destructive actions are red: Button.danger, Button.toolbar.danger for flat buttons, Button.link.danger for row links. MessageWindow turns its confirm button red when the text starts with Delete, Remove or Release. Destructive entries sit apart from everyday ones (a separator, last in a menu).
  • Button and menu labels never end with “…”, also when they open a dialog or a file picker.
  • Tables are DataGrids with RangeObservableCollection sources: virtualized, one change per batch.
  • Statuses are ui:StatusChip in a template column. Grids whose status explains a problem get Classes="wrapRows" so the text can take two lines.
  • Numbers are right-aligned: CellStyleClasses="number" and a header TextBlock.numberHeader.
  • An empty list says in one line what to do first.

Icons are resources of the client. Use them with DynamicResource in XAML (Icon="{DynamicResource Icon.refresh}"), and by key without Icon. in IconKey properties (IconKey="refresh"). Some keys:

add, remove, refresh, restart, search, settings, edit, copy, export, upload, download, file, folder, start, stop, check, close, info, key, lock, lockOpen, eye, eyeOff, shield, users, user, network, firmware, app, video, snapshot, clock, activity, toggle, zap, bell, flame, globe, grid, hardDrive, wrench, eraser, tag, plugin, clipboardCheck.

The full list is in plugins/README.md. A new icon goes into src/Oadm.Client/Themes/Icons.axaml (a 24 x 24 Lucide stroke geometry).

Texts are for installers who know Axis devices. Short and plain:

  • Intros and subtitles: one short sentence, or none. Never explain what a visible control already shows.
  • No protocol, API or internal names in UI text. Say what the user can do.
  • Tooltips only when they add something, at most about eight words.
  • Step names are imperatives with the article: “Wait for the device to come back”.
  • Errors say what happened and, when nothing was changed, “Nothing was changed”.
  • No filler: no “simply”, “easily”, “please note”.

Every member with its signature in the SDK reference: Oadm.Sdk.Client.Controls, ValidatingViewModel, FormValidator, RangeObservableCollection<T>, MessageWindow, DevicePickerWindow.