Skip to content

MaskedInput

MaskedInput derives directly from TextBox rather than introducing a lookless multi-part template: the web contract is a single native <input> wired to one @oninput handler, and the WPF port mirrors that with a single OnTextChanged override. Every edit, typing, Backspace, Delete, or paste, funnels through the same pure MaskEngine.Format pipeline, which filters characters against the mask’s digit/letter/alphanumeric/literal tokens and keeps the caret stable across the reformat.

xmlns:masked="clr-namespace:Navius.Wpf.Primitives.Controls.MaskedInput;assembly=Navius.Wpf.Primitives"
<masked:NaviusMaskedInput Mask="(000) 000-0000"
Value="{Binding Phone, Mode=TwoWay}"
PlaceholderChar="_" />
Property Type Default From Description
Mask string "" NaviusMaskedInput Mask pattern: 0 = digit, A = letter, * = alphanumeric, any other character is a fixed literal.
Value string? null NaviusMaskedInput Controlled masked value; IS this control’s Text. Two-way bindable by default via a normal binding.
DefaultValue string? null NaviusMaskedInput Uncontrolled initial value, applied once on Loaded and only when no Value is set (normalized through the mask on that first application).
PlaceholderChar char? null NaviusMaskedInput Char shown for empty slots (e.g. _); null renders nothing (a lazy skeleton). Maps to the web contract’s Placeholder (renamed to avoid colliding with the string Placeholder watermark used across Select/Combobox/TagInput).
Lazy bool true NaviusMaskedInput true: trailing fixed tokens appear only once reached. false: eager, always emitted.
Overwrite bool false NaviusMaskedInput Reserved for type-over (replace) mode. Registered but never read, a no-op stub carried forward unchanged from the web source’s own stub.
Preprocessors IReadOnlyList<Func<ElementState, ElementState>>? null NaviusMaskedInput Ordered pure transforms run on the proposed state before masking.
Postprocessors IReadOnlyList<Func<ElementState, ElementState>>? null NaviusMaskedInput Ordered pure transforms run on the masked state after masking (postfix, clamp, …).
UnmaskedValue string "" NaviusMaskedInput Read-only. The raw editable characters (placeholder-filled, no literals) behind the current masked Value.
Invalid bool false NaviusMaskedInput Presentational only (a skin hook, e.g. a destructive border). No validation logic lives here; validation is the consumer’s concern.

Disabled is not a separate DP; the control uses standard WPF IsEnabled.

Event Signature Fires when
UnmaskedValueChanged EventHandler<string> After every reformat, alongside Value updating, carrying the placeholder-filled unmasked editable characters.

Value’s own change notification is the standard WPF two-way binding path (ValueProperty with BindsTwoWayByDefault), not a separate CLR event.

No PreviewKeyDown/KeyDown handler exists anywhere in this control, matching the web contract’s single-event design: typing, Backspace, Delete, and paste all funnel through the same OnTextChanged override and are processed uniformly by MaskEngine.Format, which filters non-matching characters and recomputes the caret. The proposed caret is collapsed to the selection end before masking (a keystroke’s selection is already collapsed there; a programmatic SelectedText insertion leaves the inserted text selected with End right after it), so the pipeline never tries to preserve a non-collapsed range across a reformat.

NaviusMaskedInput adds no custom AutomationPeer: as a direct TextBox subclass, it inherits the stock TextBoxAutomationPeer (AutomationControlType.Edit, ValuePattern), which already covers the bare-native-input contract (no custom ARIA role exists in the web markup either). The unit suite exhaustively pins the caret-stability guarantee (mid-string insertion, Backspace over a fixed literal, mid-string digit deletion, paste with garbage, non-collapsed selection ends mapped independently, placeholder skeletons, clamping) directly against MaskEngine, plus control-level tests for the single-input-path wiring.

  • The web’s async JS-interop bridge (CreateMaskedSelectionAsync/GetStateAsync/SetStateAsync) and its OnAfterRenderAsync pending-caret-reapply dance, needed only to work around Blazor’s async re-render resetting the caret, disappear entirely: WPF’s TextBox.Text/CaretIndex/Select are synchronous, so the whole reformat-and-reposition happens inside one OnTextChanged hook.
  • The web’s Placeholder (char?) parameter is named PlaceholderChar on the WPF control, to avoid colliding with the string Placeholder watermark parameter used across Select, Combobox, and TagInput. Behavior is identical; only the name differs. This rename was not recorded in the original WPF implementation notes and was an M6-confirmed documentation gap, now recorded here.
  • Overwrite is carried forward as the same no-op stub the web source itself ships (“accepted for parity, not yet wired to insertion”); implementing real type-over here would diverge from the behavior this port mirrors, so it was deliberately not implemented.
  • Preprocessors/Postprocessors keep the exact Func<ElementState, ElementState> delegate contract from the web source; ElementState’s selection was flattened from a tuple to SelectionStart/SelectionEnd ints (the tuple bought nothing in WPF).
  • DefaultValue precedence mirrors the web’s “used only on first init” control flow: it seeds the control once on Loaded, only when Value is unset; afterward only Value is honored.
  • IME composition input arrives through the same OnTextChanged path (WPF raises it after composition updates), so no separate TextCompositionManager handling was added, matching the web source, which also has no dedicated composition handling.
  • data-invalid/data-disabled (presentational data attributes on the web) map to the plain Invalid bool (consumed by a template trigger) and native IsEnabled, not to Validation.HasError, preserving the web source’s “validation is the consumer’s concern” stance.
  • M6 audit found the codebase’s masking core and control wiring accurate to the doc’s claims, with one undocumented parameter rename (Placeholder to PlaceholderChar, recorded above) and no other confirmed or plausible disparities.

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

MaskedInput Gallery page in the light theme

MaskedInput Gallery page in the dark theme

MaskedInput Gallery page in the high contrast theme