The user interface¶
Avalonia from PowerShell¶
The window is Avalonia 11, hosted by PowerShell 7:
- the XAML of each window is a string, parsed at runtime
(
AvaloniaRuntimeXamlLoader.Parse): no compiled assembly of the module; - the application is started once per session, and the window runs its own dispatcher loop
(
Dispatcher.UIThread.MainLoop) until it is closed: the call blocks, as a modal console; - the data of the lists and trees are small PowerShell classes (
AvaloniaTreeNode,AvaloniaHistoryNode,AvaloniaConfigurationRow...): Avalonia binds to their properties.
The runtime¶
Resources/AvaloniaPackages.json pins the 21 NuGet packages of the window (Avalonia, Skia,
HarfBuzz, their native assets per runtime) with the SHA-512 of the nuget.org catalog. On the first
opening of a window, the console downloads the packages of the runtime of the computer, checks
each hash before extracting anything, stages them, and moves them into place at once: the module
folder when it can be written to, else the data folder of the user.
Trees and details without a window¶
New-PSGPODirectoryTree, New-PSGPOPolicyTree and Get-PSGPODirectoryDetail build what the
window shows (nodes, icons, headers, details) without any Avalonia type: they are tested without a
UI.
Icons¶
Fluent UI System Icons (MIT), size 20,
regular, in Resources/Icons: the window reads the path of each SVG as an Avalonia geometry,
available in XAML as {StaticResource Icon.<name>}. Emoji were used before, but most Linux
desktops have no font to draw them.
Pitfalls met¶
Closures
A handler made with GetNewClosure() runs in a new dynamic module: it no longer sees the
private functions of the module, nor its classes. The handlers capture what they call first
($Save = ${function:Save-PSGPODomainConfiguration}, $RowType = [AvaloniaConfigurationRow]).
The pipeline
The output of the pipeline wraps objects in PSObject, which Avalonia cannot bind: the lists
are built with List[T] and foreach. if (...) { @(...) } unrolls a list of one item: the
lists are assigned inside the branches. Select-Object wraps its objects too: a list made
from it takes .PSObject.BaseObject of each item.
The button of the view shown
The class accent of the Fluent theme does not color a button here: the windows share the
style Button.active (Get-PSGPOWindowResourceXaml, with the brushes of both themes).
The code of a window¶
Each named control of a window (Name='...' in its XAML) is in a variable of its name
($treeView, $applySettingButton...), made from the XAML in one loop
(Get-PSGPOAvaloniaNamedControl), not one line per control. The GPO console is split by view:
Start-PSGPOManagerWindow builds the window, then dot-sources each view
(Initialize-PSGPOManagerSettingView, ...HistoryView, ...WmiFilterView...), a file each, whose
state, functions and handlers live in the scope of the window while it is open.
A variable misspelt would only be $null when the window runs: PSGPOTools.WindowCode.tests.ps1
reads the code of the console (its syntax tree) and fails on a variable read that is neither
assigned nor a control of its XAML.
An error no handler caught¶
An error that escapes a handler (a click, a timer, an action posted) does not close the window: the
loop of each window (Invoke-PSGPOWindowLoop -KeepOpenOnError) takes it from
Dispatcher.UIThread.UnhandledException, shows it in a notification of the window (at the top
right, for 30 seconds), and writes it to the technical log (ERROR, with the script and the line
where it happened). The same error again within 5 seconds is said once. The GUI tests open the
windows without it: an error there stops the loop and fails the test.
Long operations in the background¶
The GPO console reads and writes some things that take seconds or minutes: Refresh (F5) and Change Domain Controller..., Detect Now (Status tab), Back up and Back up all (History tab), Save report.... They run in a worker of the console: a runspace of its own, opened as the console starts, with HGPM imported in it once (some seconds on Windows, where the module is scanned; the first operation waits for it). The window stays drawn and answers: a banner says what runs, the tree and the details of the domain wait (no other change starts meanwhile), and Cancel stops a read (not a backup, which writes and is not left half-way).
The block of an operation runs in the scope of the module in the worker: it finds the functions of
HGPM, and what it needs from the window is given as arguments. The objects of HGPM go to the
worker and back as they are, as its classes are the same types in both runspaces; the worker gets
the language and the configuration of the domain of the window when the operation starts, and the
ADMX files loaded are static members, shared already (PSGPOWindowOperation.ps1,
Initialize-PSGPOManagerOperationView). The GUI tests run the operations on the thread of the
window instead, where their mocks are; the worker has tests of its own (a result, an error, a
cancellation, a second operation refused).
Classes and runspaces
A method of a class of HGPM that calls the module runs in the runspace that defined the class,
the one of the window, even when the worker calls it (reading the files of a GPO for a
report). That runspace takes such a call between two commands of its pipeline, never while it
runs the loop of the window: the worker waited forever. While an operation runs, its timer
runs Start-Sleep -Milliseconds 0 every 5 ms, which lets the window take them. The classes
keep their affinity: with [NoRunspaceAffinity()] their methods would look for the functions
of the module in the session of the caller, where a closure or a script of the user does not
have them. Checked against the Samba lab and the Windows lab: Refresh, Detect Now and a report
in the worker, the window answering meanwhile.
Tests of the window¶
Start-PSGPOManagerWindow -OnShown { param($Ui) ... } (and Start-PSGPOPasswordPolicyWindow for
the PSO console, its window of a PSO in $Ui.Editor) opens the real window and runs the block
once it is shown, with its controls by name: the GUI tests click, type and check what the window
shows, then the window closes. They run on Windows and in the X session of a Linux computer.