Dalla documentazione di RAVAL, la libreria MIT dei libri: la stessa che sta nel suo repository.
RAva — estensioni fluenti per Avalonia in codice
Sorgenti: RAVAL\UI\ · namespace RAVAL · classe statica RAva (divisa per gruppo in più file) e classe RThemes. Spiegazione completa con esempi: TUTORIAL.md, capitolo 12.
Ogni estensione assegna una proprietà o aggiunge qualcosa al controllo e restituisce il controllo stesso, del suo tipo vero (sono generiche, T Margin<T>(this T c, ...)): dopo new TextBox().Margin(4) si ha ancora una TextBox, e la catena continua.
RAva.VStack(8,
new TextBlock().Text("Totale").FontSize(18).Bold().Foreground("#1a7fb5"),
new TextBox { PlaceholderText = "Cerca…" }.OnTextChanged(Filtra).OnEnter(Cerca),
RAva.HStack(8, new Button().Content("Annulla").OnClick(Chiudi),
new Button().Content("Salva").Classes("accent").OnClick(Salva)));
Disposizione, griglie, contenuti, clic, risorse — RAva.cs
| Estensione | Vale per | Equivale a |
Margin(tutti) · Margin(orizz, vert) · Margin(s, a, d, b) | Layoutable | Margin = new Thickness(...) |
Align(o, v) · HAlign(o) · VAlign(v) | Layoutable | HorizontalAlignment, VerticalAlignment |
Width(l) · Height(a) | Layoutable | Width, Height |
Column(c, span) · Row(r, span) | Control | Grid.SetColumn/Row (e ...Span se maggiore di 1) |
Dock(lato) | Control | DockPanel.SetDock |
Children(...) | Panel | Children.AddRange |
Text(s) | TextBlock | Text |
Content(o) | ContentControl | Content; un testo con il trattino di accesso dà anche il nome accessibile (vedi sotto) |
Child(c) | Decorator (Border...) | Child |
ToolTip(s) | Control | ToolTip.SetTip |
OnClick(metodo) | Button | Click += un metodo senza argomenti |
Resource(prop, chiave) | Control | Bind(prop, GetResourceObservable(chiave)): segue il tema |
ThemeBrush(chiave, ripiego) | Control | il pennello della risorsa nel tema attuale, per Render |
Testo, cornici, aspetto — RAvaText.cs
| Estensione | Vale per | Equivale a |
FontSize(px) | TextBlock, controlli con modello, altri Control | FontSize; su un pannello è ereditato dai figli |
FontWeight(p) · Bold() · Italic() | come sopra | FontWeight, FontStyle |
FontFamily(famiglia) | come sopra | FontFamily |
Foreground(pennello) · Foreground("#rrggbb") | come sopra | Foreground |
Wrap() · TextAlign(a) | TextBlock | TextWrapping.Wrap, TextAlignment |
Border(spessore, raggio) · CornerRadius(r) | Border, controlli con modello | BorderThickness, CornerRadius |
BorderBrush(...) | Border, controlli con modello | BorderBrush |
Padding(...) (1, 2 o 4 valori) | Border/Decorator, TextBlock, controlli con modello | Padding |
Background(...) | Panel, Border, TextBlock, controlli con modello | Background |
Visible(b) · Enabled(b) · Opacity(o) | Control | IsVisible, IsEnabled, Opacity |
Classes(nomi...) | Control | Classes.Add |
«Controlli con modello» sono i TemplatedControl: Button, TextBox, CheckBox, ListBox, Window...
Pannelli pronti — RAvaPanels.cs
| Metodo | Crea o imposta |
RAva.HStack(spazio, figli...) | StackPanel orizzontale con Spacing |
RAva.VStack(spazio, figli...) | StackPanel verticale con Spacing |
RAva.Grid(colonne, righe = "") | Grid con ColumnDefinitions/RowDefinitions scritte come in Avalonia («Auto,,2,120») |
.Spacing(s) | StackPanel.Spacing |
.Spacing(colonne, righe) | Grid.ColumnSpacing, Grid.RowSpacing |
Eventi senza lambda — RAvaEvents.cs
| Estensione | Vale per | Firma del metodo |
OnTextChanged(metodo) | TextBox | void M(string testo), a ogni modifica |
OnEnter(metodo) | TextBox | void M(); Invio ascoltato in fase Tunnel, prima della casella (vale anche con AcceptsReturn), poi segnato gestito |
OnChecked(metodo) | ToggleButton (CheckBox, ToggleSwitch, RadioButton) | void M(bool spuntato); l'indeterminato vale false |
OnSelectionChanged(metodo) | SelectingItemsControl (ListBox, ComboBox) | void M(object? scelto) |
OnThemeChanged(metodo) | Control | void M(), quando cambia ActualThemeVariant |
RAva.Every(intervallo, metodo) | — | void M(); restituisce il DispatcherTimer già avviato (fermarlo con Stop()) |
| Estensione | Vale per | Cosa fa |
OnClick(metodo) | MenuItem | Click += un metodo senza argomenti, come per i Button |
HotKey("Ctrl+S") | MenuItem | la scorciatoia: InputGesture (la scrive a destra della voce) e HotKey (la fa funzionare) |
var file = new MenuItem { Header = "_File" };
file.Items.Add(new MenuItem { Header = "_Salva" }.HotKey("Ctrl+S").OnClick(Salva));
OnClick del MenuItem non è generica: un secondo OnClick<T> con un vincolo diverso avrebbe la stessa firma di quello dei Button (errore CS0111). Così su un Button il compilatore sceglie la generica, su un MenuItem questa.
Accessibilità — RAvaAccess.cs
| Estensione | Vale per | Cosa fa |
AutomationName(s) | Control | AutomationProperties.SetName: il nome che il lettore di schermo (Assistente vocale, VoiceOver, Orca) annuncia |
new Button().Content("_Prenota") // si annuncia «Prenota»
new Button().Content("✕").AutomationName("Chiudi") // si annuncia «Chiudi», non «✕»
Senza un nome, Avalonia annuncia il testo del contenuto così com'è: per Button, CheckBox e RadioButton anche il trattino di accesso («_Prenota»), mentre la Label lo toglie. Per questo Content, quando riceve un testo con _, mette anche il nome senza trattini, con la regola di AccessText: _x segna la lettera, __ è un trattino vero. Il nome dato con AutomationName vince sempre, prima o dopo Content; un secondo Content aggiorna solo il nome che aveva messo lui.
Colori e temi — RAvaColors.cs, RThemes.cs
| Membro | Fa |
RAva.Brush("#1a7fb5") | SolidColorBrush da un colore come in HTML («#rgb», «#rrggbb», «#aarrggbb», nomi come «Red») |
RAva.Brush(0xFF1A7FB5) | SolidColorBrush da un numero ARGB |
new RThemes().Color(chiave, chiaro, scuro) | un colore nelle due versioni (stringhe o numeri) |
RThemes.Apply(app) · RThemes.Apply(finestra) | mette i colori in Resources.ThemeDictionaries[Light/Dark], senza togliere quelli che ci sono |
RThemes.Count | quanti colori sono stati dati |
Perché è fatta così
- Nomi inglesi, gli stessi delle proprietà. RAVAL è in inglese per tutti, come Avalonia, anche nel libro italiano (gli esempi del libro restano in italiano: i loro nomi sono del lettore). L'estensione che imposta
Margin si chiama Margin: in una chiamata con le parentesi il compilatore scarta la proprietà, che non si può chiamare, e trova l'estensione (misurato con Avalonia 12.1.4). Eccezioni: AutomationName (Name in Avalonia è un'altra cosa), gli eventi con On..., e i pannelli pronti HStack/VStack, perché Row e Column sono già le estensioni della Grid.
- Dentro
RAva i tipi omonimi si scrivono per intero. Il metodo Border nasconde la classe Border in tutta la classe RAva (errore CS0118): lì si scrive Avalonia.Controls.Border, e così per Grid, Dock, ToolTip, FontWeight, FontFamily, CornerRadius.
- Niente lambda. Ogni evento ha dietro una piccola classe privata (
Click, TextChange, Enter, Check, Selection, Tick) che ricorda il controllo e il metodo e fa da gestore. Il metodo del programma riceve già il valore utile, non mittente ed EventArgs.
- Un'estensione per tutti, con il tipo guardato a runtime.
FontSize, Padding, Background... stanno su classi diverse (TextBlock, TemplatedControl, Border, Panel) e un vincolo generico non può dire «uno di questi». Le estensioni valgono per ogni Control e scelgono la proprietà giusta in base al tipo vero.
- Errore chiaro invece di niente. Se il controllo non ha la proprietà (un
Padding su uno StackPanel) l'estensione lancia InvalidOperationException con scritto per chi vale: un'impostazione ignorata in silenzio si scoprirebbe solo guardando la finestra. Per testo e carattere invece un controllo qualsiasi va bene: si imposta la proprietà ereditata TextElement.*, che i figli prendono.
Resource invece di un colore fisso. Il colore resta legato alla chiave del dizionario del tema, così cambia da solo quando si passa dal tema chiaro allo scuro; RThemes serve solo a scrivere i due dizionari a coppie.
ThemeBrush e OnThemeChanged per chi disegna a mano. In Render non c'è binding: si legge il pennello del tema attuale al momento del disegno, e si chiama InvalidateVisual quando il tema cambia.
Inciampi
- Senza schermo i
DispatcherTimer scattano solo nel ciclo vero. In una prova con Avalonia.Headless, Dispatcher.UIThread.RunJobs() esegue i lavori in coda ma non fa scattare i timer: per provare RAva.Every bisogna far girare il ciclo con Dispatcher.UIThread.PushFrame e fermarlo da un altro thread (vedi la classe Attesa in TEST_RAVAL\Prove.cs).
- Il colore di
Resource cambia dopo un giro del dispatcher. Dopo aver cambiato RequestedThemeVariant, la proprietà legata si aggiorna solo dopo RunJobs().
- Invio e la TextBox. Una TextBox di una riga lascia passare Invio, anche nella fase Bubble; una con
AcceptsReturn = true lo usa per andare a capo e lo segna gestito, e un KeyDown normale non lo vede. OnEnter ascolta nella fase Tunnel, prima della casella, quindi funziona in tutti e due i casi, e poi segna Invio gestito: la casella non va a capo.
- Ordine dei gestori. Sullo stesso controllo i gestori della stessa fase sono chiamati dall'ultimo iscritto al primo: un gestore Tunnel iscritto prima di
OnEnter non riceve Invio (a meno di handledEventsToo: true). Misurato con Avalonia 12.1.4 in TEST_RAVAL\ProveInvio.cs, che preme Invio con la tastiera finta di Avalonia.Headless (KeyPress) e registra chi lo riceve.
InputGesture scrive il tasto, non lo fa funzionare. Su un MenuItem serve anche la proprietà HotKey (la imposta l'estensione HotKey): allora Ctrl+S sceglie la voce anche a menu chiuso e con il fuoco in una TextBox. Non va usato per Ctrl+C, Ctrl+X, Ctrl+V e Ctrl+A, che la TextBox gestisce da sé.
- Con
ItemsSource la scorciatoia di un sottomenu chiuso non funziona. Le voci date con ItemsSource diventano controlli solo quando il sottomenu si apre: prima non sono nell'albero e HotKey non viene registrato. Con Items.Add la voce è subito figlia del menu e il tasto funziona. Misurato con Avalonia 12.1.4 in TEST_RAVAL\ProveMenu.cs.
- Il lettore di schermo sente il trattino di accesso. In Avalonia 12.1.4 un
Button con Content = "_Prenota" si annuncia «_Prenota»: il trattino lo toglie solo la Label. Misurato leggendo l'albero dell'automazione (ControlAutomationPeer.CreatePeerForElement(c).GetName()) in TEST_RAVAL\ProveAccesso.cs; Content lo corregge da sé.