Aller au contenu principal

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​

ActionResult
Type $Popup opens; variables in scope first, then the function tokens
Continue typing ${paList filtered to names starting with pa (variables and functions alike)
TabFirst suggestion inserted, popup closed
↓ / ↑Navigate the list
EnterInsert highlighted suggestion
Click a suggestionInsert that suggestion
EscapeClose 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 elementVariables offered
AbstractTestStepStep locals + TestCase + TestSuite + Project + Environment
TestCaseCase locals + TestSuite + Project + Environment
TestSuiteSuite locals + Project + Environment
AutomationProjectProject 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:

FileColumn / Field
Modules/TestAutomation/Panels/AutomationItemPresenter.axamlMapping Rules → Value cell-editing template
Modules/TestAutomation/Panels/Actions/WaitMessagePanel.axamlFiltering Rules → Value cell-editing template
Controls/Editors/ManualChecksPanel.axamlManual 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​

  1. Open the AXAML file that contains the target <TextBox>.

  2. Verify the namespace xmlns:classes="using:Aethrix.Studio.Classes" is declared on the root element (many panels already have it).

  3. 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>
  4. If the DataContext chain does not contain an AbstractAction, add an explicit binding via VariableSource="{Binding ...}" as shown above.

  5. No code-behind changes are required.


Implementation reference​

Class: Aethrix.Studio.Classes.VariableAutoCompleteBehavior
File: source/Aethrix.Studio.Controls/Classes/VariableAutoCompleteBehavior.cs

Key members​

MemberDescription
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)