Skip to content

TagInput

TagInput folds the web contract’s five parts (Root/List/Field/Tag/TagRemove) into one lookless Control with two named template parts: PART_Input (a TextBox) and PART_Chips (an ItemsControl of TagChipVm). It owns the tag collection, the commit pipeline (trim, transform, duplicate/max/validate checks), delimiter-based chip creation, and a chip-navigation highlight that is a real WPF focus target rather than a virtual one.

xmlns:taginput="clr-namespace:Navius.Wpf.Primitives.Controls.TagInput;assembly=Navius.Wpf.Primitives"
<taginput:NaviusTagInput x:Name="Tags"
Value="{Binding SelectedTags, Mode=TwoWay}"
Placeholder="Add a tag" />
Property Type Default From Description
Value IReadOnlyList<string>? null NaviusTagInput The committed tags, in order. Two-way bindable by default (BindsTwoWayByDefault).
DefaultValue IReadOnlyList<string>? null NaviusTagInput Uncontrolled initial tags, applied once on Loaded when Value is unset.
Delimiters IReadOnlyList<TagDelimiter>? null NaviusTagInput Which keys/chars commit a chip. null resolves to EffectiveDelimiters = [Enter, Comma].
AllowDuplicates bool false NaviusTagInput When false, a duplicate candidate is rejected.
MaxTags int? null NaviusTagInput Caps the tag count; a candidate past the cap is rejected.
Validate Func<string, bool>? null NaviusTagInput Returning false blocks the candidate and raises TagRejected.
Transform Func<string, string>? null NaviusTagInput Normalizes a candidate after trim (e.g. lowercase).
AddOnBlur bool false NaviusTagInput Commits the field’s current text when the field loses focus.
Placeholder string? null NaviusTagInput Field placeholder text.
HighlightedIndex int -1 NaviusTagInput The chip-navigation highlight; -1 means the field holds focus.
Chips IReadOnlyList<TagChipVm>? (read-only) null NaviusTagInput The chip view-models PART_Chips renders, one per committed tag.
IsTagListEmpty bool (read-only) true NaviusTagInput True when there are no tags.
EffectiveDelimiters IReadOnlyList<TagDelimiter> (read-only) - NaviusTagInput Delimiters, or [Enter, Comma] when unset.
Method Description
CommitText(string raw) Commits raw through the pipeline (trim, transform, empty-silent, duplicate, max, validate, in that order); fires TagAdded or TagRejected. Returns a TagCommitStatus. No-op (Empty) when disabled. Directly unit-testable without a template.
RemoveTagAt(int index) Removes the chip at index; highlight (and focus, if a chip had it) moves to the next adjacent chip, or back to the field if the list empties.
RemoveHighlighted() The empty-field Backspace second press: removes the highlighted chip and returns focus to the field.
Highlight(int index, bool focus) Sets the highlight (-1 = field); when focus is true, also moves real keyboard focus.
Event Signature Fires when
TagAdded EventHandler<string> A chip is successfully committed (the web’s OnAdd).
TagRemoved EventHandler<string> A chip is removed by any path: RemoveTagCommand, chip Delete/Backspace, or the field’s Backspace two-step (the web’s OnRemove).
TagRejected EventHandler<string> A candidate is blocked by duplicate check, MaxTags, or Validate (the web’s OnInvalid).
ValueChanged EventHandler<IReadOnlyList<string>> Every committed mutation, add or remove (the web’s ValueChanged).
Command Description
NaviusTagInput.RemoveTagCommand Removes one chip; the chip’s TagChipVm is the command parameter. Bound by the remove-button template part inside each chip.
Key Behavior
Enter (field, if Enter in EffectiveDelimiters) Commits current field text as a chip when non-empty.
Tab (field, if Tab in EffectiveDelimiters) Same as Enter; Tab itself is not consumed, so focus still moves.
Comma / Space typed or pasted (field, if in delimiters) Detected on TextChanged; text is split at the delimiter, each completed segment committed, remainder stays in the field.
Backspace (field, empty) First press highlights the last chip (no focus move); a second press (with a chip already highlighted) removes it.
ArrowLeft (field, empty) Enters chip navigation: highlights and focuses the last chip.
ArrowLeft (chip) Highlight and focus the previous chip; no-op at index 0.
ArrowRight (chip) Highlight and focus the next chip, or return to the field (-1) if already at the last chip.
Home / End (chip) Highlight and focus the first/last chip.
Delete / Backspace (chip) Remove the highlighted chip.

After a chip removal, highlight (and focus, if a chip had it) moves to the next adjacent chip, or back to the field if the list becomes empty.

NaviusTagInputAutomationPeer (a FrameworkElementAutomationPeer) reports AutomationControlType.Group, since the interactive field inside is a real TextBox with its own native Edit peer. Because the committed tags live in template text (chips), not in a single control value, the root peer also implements a read-only IValueProvider: Value returns the comma-joined tag list, and SetValue throws (NaviusTagInput is read-only over ValuePattern), following the same precedent as the Select family’s value-in-template-text peer. Each remove button carries AutomationProperties.Name = “Remove {value}”. The web contract wires no ARIA roles at all for this family, so this UIA design is a fresh minimal mapping rather than a 1:1 port. The unit suite (tests/Navius.Wpf.Tests/TagInputTests.cs) exercises CommitText’s full rule order and precedence (duplicate checked before max), delimiter splitting, the Backspace two-step, and, added during the M6 audit, real PreviewKeyDown tests for field ArrowLeft chip-navigation entry and chip ArrowLeft/Home/End/ArrowRight/Delete handling.

  • Five web parts (Root/List/Field/Tag/TagRemove) fold into one Control with two template parts (PART_Input, PART_Chips); NaviusTag/NaviusTagRemove become a TagChipVm plus a DataTemplate (the Combobox chip idiom) rather than CLR control types.
  • The chip highlight is a real WPF focus target: the web’s one-shot PendingChipFocus/ PendingFieldFocus render-cycle dance collapses into synchronous Focus() calls, since WPF has no re-render round-trip to wait on.
  • Accessibility is a fresh design, not a port: the web source wires no ARIA roles for this family at all. The field gets a native TextBox Edit peer for free; the root peer adds a read-only ValuePattern (comma-joined tags) since the committed tags otherwise live only in template text, following the Select-peer precedent that such values must be exposed over UIA.
  • Value is a two-way dependency property with immutable list snapshots (the Combobox Values convention), with CLR events TagAdded/TagRemoved/TagRejected/ValueChanged standing in for the web’s OnAdd/OnRemove/OnInvalid/ValueChanged callbacks.
  • Paste-splitting on comma/space is detected on TextBox.TextChanged (which fires once with the full pasted text) rather than the web’s @oninput; both land on the same split points.

The TagInput Gallery page rendered at the pinned commit, in each theme.

TagInput Gallery page in the light theme

TagInput Gallery page in the dark theme

TagInput Gallery page in the high contrast theme