Placeholder Auto-Completion in Text Boxes
Overview
VariableAutoCompleteBehavior is an Avalonia Behavior<TextBox> that adds inline
${…} placeholder completion to any text box used in test-automation views. The user
types $ anywhere in the field; a popup immediately lists every variable accessible at
the current context level, followed by the function tokens the engine evaluates.
Typing more characters after ${ narrows both lists by prefix; pressing Tab (or
clicking an item) inserts the finished token and closes the popup.
Interaction model
| Action | Result |
|---|---|
Type $ | Popup opens; variables in scope first, then the function tokens |
Continue typing ${pa | List filtered to names starting with pa (variables and functions alike) |
| Tab | First suggestion inserted, popup closed |
| ↓ / ↑ | Navigate the list |
| Enter | Insert highlighted suggestion |
| Click a suggestion | Insert that suggestion |
| Escape | Close popup, no change |
Type } or move caret past a } | Popup closes automatically |
The replacement always covers the active token — everything from the nearest
unmatched $ before the caret to the caret — so it works correctly when the caret is
in the middle of a longer string, e.g.:
"prefix ${pa▌ suffix" → "prefix ${pageToken} suffix"
Function tokens
A ${…} placeholder is not only a variable lookup: Utilities.ParseObject first asks
whether the token is a function call (Name(args)), and evaluates it through
Aethrix.Core.Util.FunctionLibrary — matched on name plus argument count. That is how a
request gets today's date, a timestamp, a GUID or a random value without a script step,
re-evaluated on every run.
The popup lists every evaluatable overload as its signature, so the two kinds are distinguishable at a glance:
${pageToken} ← a variable in scope
${GetDateTime(format, offsetSeconds)} ← a function token
${RandomInt(min, max)}
Accepting a function inserts a closed, legal call with an example argument per parameter, and selects the FIRST argument's value inside its quotes — so the first keystroke replaces a value while the quoting and the arity survive, and accepting without typing anything still evaluates:
${RandomInt("▌1▌", "100")} → typing 5 gives ${RandomInt("5", "100")}
${GetDateTime("▌yyyy-MM-dd▌", "86400")} → the second argument is a Tab or an arrow away
${GetDate()} ← no arguments: nothing selected, caret after the token
Selecting the whole argument list instead would let one keystroke destroy the quotes that make an argument a literal, or collapse two arguments into one and break the arity the evaluator matches on.
Arguments must be quoted literals ("1", "yyyy-MM-dd") or bare variable names
(BookingReference); an unquoted literal is read as a variable and fails the rule when
nothing of that name exists. The examples the catalog inserts are quoted for that
reason. Do not put a comma, ) or } inside an argument — the tokenizer splits on
commas and ends the token at the first }.
The list is built by FunctionTokenCatalog (in Aethrix.Core.Util), which reflects over
the same internal library the evaluator matches against: a completion row cannot offer a
call the engine would refuse, and a new overload reaches the popup with no UI change.
FunctionTokenCatalogTests pins the coverage and the shape of every example.
Function tokens are evaluated wherever a mapping rule value is: automation steps,
manual filling rules, and at send time in the request URL, path and headers. They are
not evaluated inside step literals written by set_request_values /
set_request_from_json — see the assistant's functions skill for the full map.
Context-aware variable discovery
When no explicit VariableSource is bound, the behavior walks the visual ancestor
chain of the TextBox until it finds a StyledElement whose DataContext is an
AbstractAction (test step, test case, test suite, or automation project). It then
offers:
action.LocalVariables + action.GetUpperLevelParameters()
This automatically delivers the right scope:
| Context element | Variables offered |
|---|---|
AbstractTestStep | Step locals + TestCase + TestSuite + Project + Environment |
TestCase | Case locals + TestSuite + Project + Environment |
TestSuite | Suite locals + Project + Environment |
AutomationProject | Project locals + Environment |
Inside a DataGrid whose rows are MappingRule objects the DataGrid's own
DataContext (the enclosing test step) is found further up the tree — the walk skips
MappingRule rows automatically.
Wiring a text box (minimal — zero binding)
Add the behavior declaration inside <Interaction.Behaviors> on any TextBox. No
additional binding is required when the text box lives inside a view whose DataContext
is (or inherits) an AbstractAction.
1. Add the classes namespace to the enclosing control/window (if not present)
xmlns:classes="using:Aethrix.Studio.Classes"
2. Attach the behavior
<TextBox Text="{Binding SomeProperty}">
<Interaction.Behaviors>
<classes:VariableAutoCompleteBehavior />
</Interaction.Behaviors>
</TextBox>
That is all. The behavior finds the AbstractAction ancestor automatically.
Wiring with an explicit variable source
If the automatic discovery does not resolve to the right scope — for example, when the
text box is rendered inside a custom dialog or a control where the DataContext chain
does not contain an AbstractAction — bind VariableSource explicitly:
<!-- MyDialog.axaml (DataContext = SomeViewModel) -->
<TextBox Text="{Binding Value}">
<Interaction.Behaviors>
<classes:VariableAutoCompleteBehavior
VariableSource="{Binding AvailableVariables}" />
</Interaction.Behaviors>
</TextBox>
VariableSource accepts any IEnumerable<Variable>. A common helper:
// In your ViewModel or code-behind:
public IEnumerable<Variable> AvailableVariables =>
_testStep.LocalVariables.Concat(_testStep.GetUpperLevelParameters());
Existing wired text boxes
The following text boxes already have VariableAutoCompleteBehavior attached:
| File | Column / Field |
|---|---|
Modules/TestAutomation/Panels/AutomationItemPresenter.axaml | Mapping Rules → Value cell-editing template |
Modules/TestAutomation/Panels/Actions/WaitMessagePanel.axaml | Filtering Rules → Value cell-editing template |
Controls/Editors/ManualChecksPanel.axaml | Manual filling rule → Value cell-editing template |
All three resolve through Utilities.ParseObject, so all three evaluate function tokens.
Adding completion to more text boxes — checklist
-
Open the AXAML file that contains the target
<TextBox>. -
Verify the namespace
xmlns:classes="using:Aethrix.Studio.Classes"is declared on the root element (many panels already have it). -
Wrap the existing
<TextBox>content inside<Interaction.Behaviors>:<TextBox Text="{Binding MyField, UpdateSourceTrigger=LostFocus}"><Interaction.Behaviors><classes:VariableAutoCompleteBehavior /></Interaction.Behaviors><!-- existing ContextFlyout etc. stays here --></TextBox> -
If the DataContext chain does not contain an
AbstractAction, add an explicit binding viaVariableSource="{Binding ...}"as shown above. -
No code-behind changes are required.
Implementation reference
Class: Aethrix.Studio.Classes.VariableAutoCompleteBehavior
File: source/Aethrix.Studio.Controls/Classes/VariableAutoCompleteBehavior.cs
Key members
| Member | Description |
|---|---|
VariableSource (StyledProperty<IEnumerable<Variable>?>) | Optional override for the variable list; auto-discovered when null. Function tokens are always offered and are not affected by it |
OnAttached() | Creates the Popup + ListBox and subscribes to TextChanged, KeyDown, LostFocus |
OnDetaching() | Unsubscribes events, closes popup, releases resources |
GetActiveToken(text, caretIndex) | Returns the $… token being typed before the caret, or null if the caret is not inside an open variable expression |
GetCompletionItems() | The popup's rows: variables in scope (deduplicated) then FunctionTokenCatalog.Tokens |
AcceptSuggestion(item) | Replaces the active token with the item's Insertion, then selects its SelectionStart/SelectionLength span |
GetVariables() | Returns VariableSource if set; otherwise walks the visual tree |
Row type: Aethrix.Studio.Classes.PlaceholderCompletionItem (same folder) — one row for either kind,
carrying the text to insert and the span to select.
Function source: Aethrix.Core.Util.FunctionTokenCatalog — reflects over the internal
FunctionLibrary the evaluator matches against, so the list cannot offer a call that would fail, and
carries a legal quoted example per argument. Pinned by Aethrix.Test/Core/FunctionTokenCatalogTests.cs.
Token parsing rules
The active token spans from the last unmatched $ before the caret to the caret
itself. Encountering a } between the $ and the caret invalidates the token (the
expression is already closed).
"val ${ab▌c" → token = "${ab", filter = "ab"
"val ${abc}▌ xy" → no token (caret is after the closing brace)
"val $▌" → token = "$", filter = "" (shows all variables)