NumericInput.Core
1.3.1
See the version list below for details.
dotnet add package NumericInput.Core --version 1.3.1
NuGet\Install-Package NumericInput.Core -Version 1.3.1
<PackageReference Include="NumericInput.Core" Version="1.3.1" />
<PackageVersion Include="NumericInput.Core" Version="1.3.1" />
<PackageReference Include="NumericInput.Core" />
paket add NumericInput.Core --version 1.3.1
#r "nuget: NumericInput.Core, 1.3.1"
#:package NumericInput.Core@1.3.1
#addin nuget:?package=NumericInput.Core&version=1.3.1
#tool nuget:?package=NumericInput.Core&version=1.3.1
NumericInput — WPF Numeric Input Control
A C# / WPF (.NET 8 · .NET 10) library for entering and displaying physical numeric values with automatic unit conversion, culture-aware parsing, expression evaluation, and a full set of UX helpers inspired by Gu.Wpf.NumericInput.
Table of Contents
- Packages: Core vs WPF
- Installation
- Solution structure
- Quick start
- Dependency Properties
- NumericBox — inheritable attached properties
- Commands
- Culture support and limitations
- Expression evaluator
- Physics families and units
- Feature reference
- Build and test
- Adding a new unit
- Unit systems
- Flowcharts
Packages: Core vs WPF
This repository publishes two separate NuGet packages. Pick the one that matches what you're building:
NumericInput.Core |
NumericInput.WPF |
|
|---|---|---|
| What it is | Platform-agnostic class library (net8.0, net10.0) |
WPF UserControl (net8.0-windows, net10.0-windows) |
| Contains | UnitRegistry, UnitConverter, ExpressionEvaluator, PhysicsFamily, [UserInterface] attribute |
NumericInputControl, NumericBox, converters, validation/spinner enums |
| Depends on | Nothing (no WPF, no UI framework) | NumericInput.Core (pulled in automatically) |
| Use it when… | You need unit conversion, physical-value parsing, or the expression evaluator in a non-UI project (console app, service, ViewModel-only library, another UI stack) | You're building a WPF app and want the ready-made numeric input control |
In short: NumericInput.Core is the engine (units, conversion, parsing, expression evaluation — no UI at all). NumericInput.WPF is the WPF control built on top of it. Most WPF applications only need to reference NumericInput.WPF — NumericInput.Core comes along transitively via NuGet.
You'd reference NumericInput.Core on its own only if you want the conversion/expression engine without any WPF dependency (e.g. a backend service or a different UI framework).
Installation
# WPF app — installs NumericInput.WPF and NumericInput.Core together
dotnet add package NumericInput.WPF
# Non-UI project — installs only the platform-agnostic engine
dotnet add package NumericInput.Core
<ItemGroup>
<PackageReference Include="NumericInput.WPF" Version="1.2.1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NumericInput.Core" Version="1.2.1" />
</ItemGroup>
Solution structure
NumericInput.sln
├── NumericInput.Core ← platform-agnostic library (net8.0 · net10.0)
│ ├── PhysicsFamily.cs ← enum of physical families
│ ├── UnitRegistry.cs ← catalogue of 100+ units + lookup
│ ├── UnitConverter.cs ← conversion, parsing "100 bar", formatting
│ ├── ExpressionEvaluator.cs ← arithmetic/trig expression evaluator
│ └── UserInterfaceAttribute.cs ← [UserInterface(…)] + PropertyMetadata
│
├── NumericInput.WPF ← WPF library (net8.0-windows · net10.0-windows)
│ ├── NumericInputControl.xaml / .xaml.cs ← main UserControl
│ ├── NumericBox.cs ← inheritable attached properties
│ ├── SpinUpdateMode.cs ← enum (PropertyChanged / AsBinding)
│ ├── ValidationTrigger.cs ← enum (LostFocus / PropertyChanged)
│ └── Converters/ValueConverters.cs
│
├── NumericInput.Example ← sample WPF app (PressureVessel)
│
├── NumericInput.Tests ← xUnit test suite (788 tests)
│ ├── NumericInputTests.cs
│ ├── CultureParseTests.cs ← 73 culture tests
│ └── CultureExtendedTests.cs ← 413 tests · 32 cultures
└── NumericInput.WPF.Tests ← xUnit WPF regression tests for `NumericInputControl`
└── NumericInputWpfTests.cs
Flowcharts
Detailed A3 flowcharts for NumericInputControl are available in:
docs/NumericInputControl-Flowcharts/README.mddocs/NumericInputControl-Flowcharts/Sheet-01-Macro-Overview.svgdocs/NumericInputControl-Flowcharts/Sheet-02-Bind-And-Refresh.svgdocs/NumericInputControl-Flowcharts/Sheet-03-Commit-Validation-Conversion.svgdocs/NumericInputControl-Flowcharts/Sheet-04-Interaction-And-Utilities.svg
Quick start
1. Decorate the model with [UserInterface]
using NumericInput.Core;
public class PressureVessel : INotifyPropertyChanged
{
[UserInterface(
Description = "Design Pressure",
UoM = "Pa", // native (invariant) unit
PhysicsFamily = PhysicsFamily.Pressure,
DecimalPlaces = 3,
MinValue = 0,
MaxValue = 25_000_000,
TooltipText = "ASME VIII max operating pressure")]
public double DesignPressure { get; set; } = 1_000_000; // 1 MPa
}
2. Add the namespace and control in XAML
<Window
xmlns:ni="clr-namespace:NumericInput.WPF;assembly=NumericInput.WPF"
xmlns:glob="clr-namespace:System.Globalization;assembly=mscorlib">
<ni:NumericInputControl
x:Name="ctrlPressure"
DescriptionWidth="160"
UoMWidth="60"
MinTextWidth="90"
MaxTextWidth="300"
DisplayUoMSymbol="bar"
AllowSpinners="True"
Increment="0.5"
SelectAllOnFocus="True"
MoveFocusOnEnter="True"/>
</Window>
3. Bind in code-behind
var vessel = new PressureVessel();
ctrlPressure.BindTo(vessel, nameof(PressureVessel.DesignPressure));
BindTo reads [UserInterface], sets Description / native unit / family / decimals,
subscribes to INotifyPropertyChanged, and shows the value converted to bar.
Dependency Properties
Layout
| Property | Type | Default | Description |
|---|---|---|---|
Description |
string |
"" |
Left label text |
DescriptionWidth |
double |
130 |
Fixed width of the description label (px) |
DescriptionTooltipText |
string |
"" |
Text shown in the popup when user clicks the description label |
IsDescriptionTooltipEnabled |
bool |
true |
Enables the description click-popup |
DisplayUoMSymbol |
string |
"" |
Unit symbol shown in the UoM label |
UoMWidth |
double |
60 |
Fixed width of the UoM label (px) |
ErrorIconWidth |
double |
24 |
Fixed width of the error icon (px) |
MinTextWidth |
double |
60 |
Minimum width of the text input column (px) |
MaxTextWidth |
double |
500 |
Maximum width of the text input column (px) |
IsTextWidthLocked |
bool |
false |
Locks the TextBox column to a fixed pixel width (useful for aligned stacked controls) |
Value and binding
| Property | Type | Default | Description |
|---|---|---|---|
ValueDouble |
double |
NaN |
Current value in display units (two-way bindable) |
PhysicsFamily |
PhysicsFamily |
Unknown |
Physical family — filters the unit drop-down |
HasError |
bool |
false |
True when the last commit produced a validation error |
ErrorText |
string |
"" |
Error message shown in the popup |
MinValue |
double |
NaN |
Minimum accepted value in native units (NaN = no limit) |
MaxValue |
double |
NaN |
Maximum accepted value in native units (NaN = no limit) |
CanValueBeNull |
bool |
false |
When true, an empty TextBox writes null to the bound property |
Formatting
| Property | Type | Default | Description |
|---|---|---|---|
DecimalPlaces |
int |
4 |
Decimal places shown (used when StringFormat and DecimalDigits are not set) |
GroupDigits |
bool |
true |
Include thousands separator in the formatted output |
UseInvariantCulture |
bool |
false |
Force dot as decimal separator regardless of the system culture |
Culture |
CultureInfo? |
null |
Explicit culture for parsing and formatting — highest priority (see Culture priority) |
StringFormat |
string? |
null |
Free-form format string passed to value.ToString(fmt, culture) — overrides DecimalPlaces / GroupDigits |
DecimalDigits |
int? |
null |
Decimal digits (positive = places shown; negative = rounding magnitude, e.g. -2 rounds to nearest 100) |
Formatting priority (highest → lowest)
StringFormat → DecimalDigits → DecimalPlaces + GroupDigits
StringFormat examples
| XAML | Input | Output (en-US) |
|---|---|---|
StringFormat="F2" |
1234.5 | 1234.50 |
StringFormat="N3" |
1234.5 | 1,234.500 |
StringFormat="E4" |
1234.5 | 1.2345E+003 |
StringFormat="P0" |
0.875 | 88% |
StringFormat="0.00" |
1234.5 | 1234.50 |
StringFormat="#,##0.##" |
1234.5 | 1,234.5 |
DecimalDigits examples
DecimalDigits |
Input | Rounded value | Display |
|---|---|---|---|
2 |
1234.5678 | — | 1,234.57 |
0 |
1234.5678 | — | 1,235 |
-1 |
1234.5678 | 1230 | 1,230 |
-2 |
1234.5678 | 1200 | 1,200 |
-3 |
1234.5678 | 1000 | 1,000 |
Input validation
| Property | Type | Default | Description |
|---|---|---|---|
NumberStyles |
NumberStyles |
Float, AllowThousands, AllowLeadingSign |
Which numeric patterns are accepted at parse time |
RegexPattern |
string |
"" |
Optional regex applied to raw text before parsing (100 ms timeout) |
ValidationTrigger |
ValidationTrigger |
LostFocus |
When validation runs (see below) |
NumberStyles examples
<ni:NumericInputControl NumberStyles="Integer"/>
<ni:NumericInputControl NumberStyles="AllowDecimalPoint, AllowThousands"/>
<ni:NumericInputControl NumberStyles="Float, AllowThousands, AllowLeadingSign"/>
ValidationTrigger
| Value | Behaviour |
|---|---|
LostFocus (default) |
Validate and commit on LostFocus or Enter. Invalid intermediate input is silently ignored while typing. |
PropertyChanged |
Validate on every keystroke. Valid input commits immediately; errors show without restoring the display so the user can keep editing. A final commit/restore still runs on LostFocus. |
UX helpers
| Property | Type | Default | Description |
|---|---|---|---|
SelectAllOnFocus |
bool |
true |
Select all text when the TextBox receives keyboard focus |
SelectAllOnClick |
bool |
false |
Select all text on mouse click |
SelectAllOnDoubleClick |
bool |
false |
Select all text on double-click |
MoveFocusOnEnter |
bool |
false |
Move focus to next control after a successful Enter commit |
Spinner
| Property | Type | Default | Description |
|---|---|---|---|
AllowSpinners |
bool |
false |
Show the ▲▼ spinner buttons |
Increment |
double |
1.0 |
Step size per click; clamped to MinValue/MaxValue |
SpinUpdateMode |
SpinUpdateMode |
PropertyChanged |
When the spinner writes back to the bound object |
SpinUpdateMode
| Value | Behaviour |
|---|---|
PropertyChanged (default) |
Each click immediately writes the new value to the bound object. |
AsBinding |
The TextBox is updated at each click, but the bound object is only written on LostFocus or Enter — same trigger as manual text editing. |
NumericBox — inheritable attached properties
The NumericBox static class exposes every key setting as an inheritable attached property.
Setting one on a parent container (Window, Grid, StackPanel…) propagates it to all
descendant NumericInputControl instances without having to repeat the attribute on each one.
A locally set instance property always wins over an inherited value.
xmlns:ni="clr-namespace:NumericInput.WPF;assembly=NumericInput.WPF"
xmlns:glob="clr-namespace:System.Globalization;assembly=mscorlib"
Culture for an entire form
<Grid ni:NumericBox.Culture="{x:Static glob:CultureInfo.InvariantCulture}">
<ni:NumericInputControl />
<ni:NumericInputControl />
</Grid>
Spinner buttons for all controls
<StackPanel ni:NumericBox.AllowSpinners="True"
ni:NumericBox.SpinUpdateMode="PropertyChanged">
<ni:NumericInputControl Increment="0.1" />
<ni:NumericInputControl Increment="10" />
<ni:NumericInputControl AllowSpinners="False" />
</StackPanel>
Uniform formatting across a panel
<Grid ni:NumericBox.StringFormat="F2">
<ni:NumericInputControl />
<ni:NumericInputControl StringFormat="N4" />
</Grid>
Keyboard UX on a Window level
<Window ni:NumericBox.SelectAllOnGotKeyboardFocus="True"
ni:NumericBox.MoveFocusOnEnter="True"
ni:NumericBox.ValidationTrigger="PropertyChanged">
</Window>
Complete attached property reference
| Attached Property | Type | Description |
|---|---|---|
NumericBox.Culture |
CultureInfo? |
Explicit culture for parsing and formatting |
NumericBox.NumberStyles |
NumberStyles? |
Allowed numeric patterns |
NumericBox.CanValueBeNull |
bool? |
Empty input → null |
NumericBox.StringFormat |
string? |
Free-form format string |
NumericBox.DecimalDigits |
int? |
Decimal places / rounding magnitude |
NumericBox.AllowSpinners |
bool? |
Show spinner buttons |
NumericBox.SpinUpdateMode |
SpinUpdateMode? |
When spinners write to the bound object |
NumericBox.MoveFocusOnEnter |
bool? |
Move focus on Enter |
NumericBox.SelectAllOnGotKeyboardFocus |
bool? |
Select all on keyboard focus |
NumericBox.SelectAllOnClick |
bool? |
Select all on mouse click |
NumericBox.SelectAllOnDoubleClick |
bool? |
Select all on double-click |
NumericBox.ValidationTrigger |
ValidationTrigger? |
When validation runs |
Commands
Two RoutedUICommand are exposed as static members of NumericInputControl:
| Command | Description |
|---|---|
NumericInputControl.IncrementCommand |
Increases the value by Increment (same as clicking ▲) |
NumericInputControl.DecrementCommand |
Decreases the value by Increment (same as clicking ▼) |
Both commands respect MinValue / MaxValue bounds and SpinUpdateMode.
CanExecute returns false when the control is disabled or read-only.
MVVM binding example
<Button Content="▲" Command="{x:Static ni:NumericInputControl.IncrementCommand}"
CommandTarget="{Binding ElementName=myControl}"/>
<Button Content="▼" Command="{x:Static ni:NumericInputControl.DecrementCommand}"
CommandTarget="{Binding ElementName=myControl}"/>
<ni:NumericInputControl x:Name="myControl" AllowSpinners="True" Increment="5"/>
Key binding example
<ni:NumericInputControl x:Name="myControl" AllowSpinners="True" Increment="1">
<ni:NumericInputControl.InputBindings>
<KeyBinding Key="Up" Command="{x:Static ni:NumericInputControl.IncrementCommand}"
CommandTarget="{Binding ElementName=myControl}"/>
<KeyBinding Key="Down" Command="{x:Static ni:NumericInputControl.DecrementCommand}"
CommandTarget="{Binding ElementName=myControl}"/>
</ni:NumericInputControl.InputBindings>
</ni:NumericInputControl>
Culture support and limitations
Culture priority (highest → lowest)
Culture (instance DP)
│ set explicitly on the control: Culture="it-IT"
▼
NumericBox.Culture (inherited attached)
│ set on a parent container, propagates to all children
▼
UseInvariantCulture = true
│ forces CultureInfo.InvariantCulture (dot decimal, no grouping)
▼
CultureInfo.CurrentCulture
system locale — the default when nothing else is set
Setting culture in XAML
<ni:NumericInputControl
Culture="{x:Static glob:CultureInfo.GetCultureInfo('it-IT')}"/>
<ni:NumericInputControl UseInvariantCulture="True"/>
<ni:NumericInputControl
Culture="{x:Static glob:CultureInfo.InvariantCulture}"/>
<Grid>
<Grid.Resources>
<glob:CultureInfo x:Key="Italian">it-IT</glob:CultureInfo>
</Grid.Resources>
</Grid>
Disambiguation rules for the number parser
The parser accepts both 1,234.5 (English) and 1.234,5 (Italian) in the same
control, choosing the right interpretation based on context:
| Separators found | Rule |
|---|---|
Both . and , |
The last separator is the decimal separator |
Single ,, not exactly 3 digits after it |
Comma is decimal → 1,5 → 1.5 |
Single ,, exactly 3 digits after it |
Decimal if culture uses , as decimal; otherwise thousands |
Single ., not exactly 3 digits after it |
Dot is decimal → 1.5 → 1.5 |
Single ., exactly 3 digits after it |
Thousands if culture uses , as decimal; otherwise decimal |
| Multiple same separators | All are thousands → 1.234.567 → 1234567 |
Supported culture groups
Group A — Comma decimal, dot thousands (fully supported ✅)
Cultures: it-IT, de-DE, es-ES, pt-PT, pt-BR, pl-PL, hr-HR, cs-CZ,
sk-SK, hu-HU, ro-RO, bg-BG, el-GR, sl-SI, uk-UA, tr-TR, ar-SA
Format: 1.234,56
Parse: "1.234,56" → 1234.56 ✅
Parse: "1234,56" → 1234.56 ✅
Parse: "1234.56" → 1234.56 ✅ (invariant format accepted)
Parse: "1,234.56" → 1234.56 ✅ (both separators → last is decimal)
Group B — Comma decimal, NBSP thousands ⚠️
Cultures: fr-FR, fr-BE, sv-SE, fi-FI, nb-NO, da-DK, nl-BE,
et-EE, lt-LT, lv-LV
Format (small): 123,45 → parses back correctly ✅
Format (large): 1 234,56 → NBSP separator — cannot be re-parsed ⚠️
Workaround: GroupDigits="False" eliminates the NBSP grouping
Limitation: The parser regex recognises only
.and,as separator characters. Non-breaking space (U+00A0), used by French and Nordic cultures as the thousands separator, is not matched. Small numbers (< 1000) and numbers formatted without digit grouping round-trip correctly. Large formatted numbers must be re-entered without the NBSP separator.Recommended fix: set
GroupDigits="False"orStringFormat="F2"for these cultures to suppress the thousands separator in the display output.
<Grid ni:NumericBox.Culture="{x:Static glob:CultureInfo.GetCultureInfo('fr-FR')}"
ni:NumericBox.StringFormat="F2">
<ni:NumericInputControl />
</Grid>
Group C — Dot decimal, comma thousands (fully supported ✅)
Cultures: en-US, en-GB, en-AU, nl-NL, ja-JP, zh-CN, ms-MY
Format: 1,234.56
Parse: "1,234.56" → 1234.56 ✅
Parse: "1234.56" → 1234.56 ✅
Parse: "1.234,56" → 1234.56 ✅ (both separators → last is decimal)
Group D — Dot decimal, apostrophe thousands ⚠️
Cultures: de-CH, fr-CH, it-CH
Format: 1'234.56 → RIGHT SINGLE QUOTATION MARK (U+2019)
Parse: "1'234.56" → FAILS — apostrophe not in separator regex ⚠️
Parse: "1234.56" → 1234.56 ✅
Parse: "1,234.56" → 1234.56 ✅ (comma treated as thousands)
Limitation: The Swiss apostrophe (U+2019) used by
de-CH/fr-CH/it-CHis not a recognised thousands separator. Numbers formatted by the system (e.g.1'234.56) cannot be re-parsed. Plain1234.56and comma-grouped1,234.56work correctly.Recommended fix:
GroupDigits="False"orStringFormat="F2".
Indian grouping ⚠️
Cultures: en-IN, hi-IN
Format: 1,00,000 → 2-2-3 digit grouping
Parse: "1,00,000" → read as 1.0 + spurious token — FAILS ⚠️
Parse: "100000" → 100000 ✅
Parse: "1,00,000 N" → FAILS ⚠️
Limitation: Indian numeric grouping uses 2-digit sub-groups after the first three digits (
1,00,000for one lakh). The parser expects groups of exactly 3 digits. Users should enter numbers without the thousands separator, or use standard 3-digit grouping (100,000).
Summary table
| Culture group | Examples | Fully supported | Limitation |
|---|---|---|---|
A — , decimal / . thousands |
it-IT de-DE es-ES pl-PL |
✅ Yes | — |
B — , decimal / NBSP thousands |
fr-FR sv-SE fi-FI nb-NO |
⚠️ Partial | Large formatted numbers not re-parseable |
C — . decimal / , thousands |
en-US en-GB ja-JP zh-CN |
✅ Yes | — |
D — . decimal / ' thousands |
de-CH fr-CH it-CH |
⚠️ Partial | Apostrophe not parseable as separator |
| Indian 2-2-3 grouping | en-IN hi-IN |
⚠️ Partial | 2-digit groups not recognised |
| InvariantCulture | — | ✅ Yes | — |
Expression evaluator
Prefix the input with = to evaluate an arithmetic or trigonometric expression.
The evaluator always uses InvariantCulture: dot (.) for decimal, comma (,)
to separate function arguments.
=1.5+2 → 3.5
=100/3 → 33.333…
=sqrt(2)*50 → 70.711…
=sin(pi/4) → 0.7071…
=round(3.14159,2) → 3.14
=clamp(150,0,100) → 100
Supported operators
| Token | Meaning |
|---|---|
+ - * / |
Arithmetic |
^ |
Power (right-associative) |
- + (unary) |
Sign |
( ) |
Grouping |
Constants
| Token | Value |
|---|---|
pi |
π ≈ 3.14159… |
e |
e ≈ 2.71828… |
phi |
φ ≈ 1.61803… |
Functions
| Category | Functions |
|---|---|
| Trigonometry (rad) | sin cos tan asin acos atan atan2 sinh cosh tanh |
| Trigonometry (deg) | sind cosd tand |
| Power / root | sqrt cbrt sq cube pow exp |
| Logarithm | ln log log10 log2 |
| Rounding | round floor ceil trunc |
| Misc | abs sign min max clamp hypot |
Physics families and units
Families
Adimensional · Ratio · Percentage · Angle ·
Length · Area · Volume · SecondMomentOfArea · SectionModulus ·
Force · Moment · Pressure · Stress · Energy · Power ·
Mass · LinearDensity · SurfaceDensity · VolumeDensity ·
Temperature · TemperatureInterval · ThermalConductivity · HeatFlux ·
Time · Frequency · Velocity · Acceleration ·
ElectricVoltage · ElectricCurrent · ElectricResistance ·
SpecificHeat · SpecificEnthalpy · SpecificEntropy ·
DynamicViscosity · KinematicViscosity · ThermalDiffusivity ·
MomentumFlux · MassFlow · VolumetricFlow ·
FilmCoefficient · FoulingResistance
A temperature difference is not a temperature
Temperature is affine: °C carries an offset of 273.15, which is right for a
reading and wrong for a difference. A ΔT of 40 K is 40 °C — the two scales
share a degree and differ only in origin.
Use TemperatureInterval for a difference. It is purely multiplicative, so
K and Δ°C both carry a factor of one and Δ°F carries 5/9.
// 40 K as a difference
UnitConverter.Convert(40, UnitRegistry.Get("K", PhysicsFamily.TemperatureInterval),
UnitRegistry.Get("Δ°C", PhysicsFamily.TemperatureInterval)); // 40
// 313.15 K as a reading
UnitConverter.Convert(313.15, UnitRegistry.Get("K"), UnitRegistry.Get("°C")); // 40
Two families can share a symbol
MPa is both a pressure and a stress; m³ is both a volume and a section
modulus. A plain TryGet returns whichever family is declared first, so pass
the family when you mean the other one:
UnitRegistry.Get("MPa").Family; // Pressure
UnitRegistry.Get("MPa", PhysicsFamily.Stress).Family; // Stress
The family-scoped overload also guarantees the unit it returns belongs to the family you asked for, so a conversion built from it cannot fail on a mismatch.
Symbols are case-sensitive first
Lookup tries an exact-case match before falling back to a case-insensitive
one. This is what tells mN (millinewton) from MN (meganewton), mW from
MW, cal from Cal (the food Calorie, which is one kilocalorie), kn
(knot) from kN (kilonewton), and Gal (galileo) from gal (US gallon).
Typing mpa still finds MPa.
Unit conversion model
base_value = value × Factor + Offset (to base unit)
value = (base_value − Offset) / Factor (from base unit)
Offset is non-zero only for temperature (°C, °F, K, °R).
Inline unit parsing
The TextBox accepts a number followed by an optional unit symbol. The unit is converted to the native unit and then to the display unit.
Control: Display = MPa, Native = Pa
"100 bar" → 100 bar = 10 000 000 Pa = 10.000 MPa ✅
"2500 psi" → 2500 psi ≈ 17.237 MPa ✅
"100 °C" → Error: incompatible family (Temperature) ❌
"=100/3" → 33.333… MPa (expression) ✅
Feature reference
| # | Feature | DP / API | Notes |
|---|---|---|---|
| A | Horizontal resize via Thumb | — | Right-edge drag handle |
| B | Vertical alignment | — | All sub-controls centred on the same baseline |
| C | Fixed-width columns | DescriptionWidth UoMWidth ErrorIconWidth |
|
| D | Locale-aware formatting | Culture UseInvariantCulture DecimalPlaces StringFormat DecimalDigits GroupDigits |
|
| E | Two-way binding | BindTo(obj, name) · ValueDouble |
INPC-aware |
| F | Automatic unit conversion | DisplayUoMSymbol ChangeDisplayUnit() |
|
| G | Unit catalogue | UnitRegistry · UnitConverter |
100+ units |
| H | Inline unit parsing | "100 bar" syntax |
|
| I | Unit drop-down | Click UoM label | Context menu per family |
| J | Immutable native unit | _nativeUnit |
Set once via [UserInterface] or BindTo |
| K | Ctrl+C / Ctrl+V | — | Copy = invariant number; Paste = commit |
| L | Error icon + popup | HasError ErrorText |
Click icon to toggle popup |
| M | [UserInterface] attribute |
Description UoM PhysicsFamily DecimalPlaces MinValue MaxValue TooltipText IsReadOnly |
Auto-populates on BindTo |
| N | Expression evaluator | =… prefix |
Arithmetic + trig, InvariantCulture |
| O | Description tooltip | DescriptionTooltipText IsDescriptionTooltipEnabled |
Click label to open popup |
| P | Locked TextBox width | IsTextWidthLocked |
Aligns stacked controls |
| Q | Bounds check | MinValue MaxValue |
Priority over [UserInterface] |
| R | Select all on focus | SelectAllOnFocus |
Default true |
| S | Select all on click/dbl-click | SelectAllOnClick SelectAllOnDoubleClick |
|
| T | Move focus on Enter | MoveFocusOnEnter |
Cycles to next focusable control |
| U | Spinner ▲▼ | AllowSpinners Increment SpinUpdateMode |
RepeatButton with Delay/Interval |
| V | Null value support | CanValueBeNull |
Empty → null on bound property |
| W | Culture override | Culture UseInvariantCulture |
Priority chain (see above) |
| X | NumberStyles | NumberStyles |
Restrict sign / decimal / exponent |
| Y | Regex validation | RegexPattern |
Applied before numeric parsing |
| Z | Validation trigger | ValidationTrigger |
LostFocus or PropertyChanged |
| Z1 | Commands | IncrementCommand DecrementCommand |
RoutedUICommand |
| Z2 | Inherited attached props | NumericBox.* |
Set once on container, propagates to children |
Build and test
# Restore and build the solution in Release mode
dotnet restore NumericInput.sln
dotnet build NumericInput.sln --configuration Release
# Run the full test suite (788 tests)
dotnet test NumericInput.Tests/NumericInput.Tests.csproj --configuration Release
# Launch the example application
dotnet run --project NumericInput.Example/NumericInput.Example.csproj --configuration Debug
# Run the WPF control regression tests
dotnet test NumericInput.WPF.Tests/NumericInput.WPF.Tests.csproj --configuration Debug
Debugging
- Open the solution in Visual Studio or Visual Studio Code.
- In VS Code, use the
Debugpanel and launch theExampleapp configuration, or rundotnet buildthen attach the debugger to the running process.
// .vscode/launch.json example
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Example App",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "🔨 Build — Debug (soluzione completa)",
"program": "${workspaceFolder}/NumericInput.Example/bin/Debug/net8.0-windows/NumericInput.Example.exe",
"cwd": "${workspaceFolder}/NumericInput.Example",
"console": "integratedTerminal",
"windows": {
"command": "${workspaceFolder}/NumericInput.Example/bin/Debug/net8.0-windows/NumericInput.Example.exe"
}
}
]
}
- In VS Code, the task name must match the
.vscode/tasks.jsonlabel:🔨 Build — Debug (soluzione completa). - To debug the control itself, set breakpoints in
NumericInput.WPF/NumericInputControl.xaml.cs,NumericInput.WPF/NumericBox.cs, orNumericInput.Core/UnitConverter.cs. - Use
dotnet test --filterto run a smaller subset of tests while iterating.
# Run a single test class
dotnet test NumericInput.Tests/NumericInput.Tests.csproj --filter "FullyQualifiedName~NumericInputTests"
Publish
- The example app formatting/culture/validation/UX demo controls use
PhysicsFamily="Adimensional"when no physical unit is configured, so raw unitless numeric input is parsed and displayed correctly.
# Publish the example WPF app for a chosen runtime
dotnet publish NumericInput.Example/NumericInput.Example.csproj \
--configuration Release \
--runtime win-x64 \
--self-contained true \
-p:PublishSingleFile=true \
-o publish/win-x64-sc
- Change
--runtimetowin-x86orwin-arm64as needed. - Set
--self-contained falsefor a framework-dependent build. - The output folder is
publish/<runtime>-<mode>when using the workspace tasks.
Pack for NuGet
# Pack the core library
dotnet pack NumericInput.Core/NumericInput.Core.csproj --configuration Release --output publish/nuget
# Pack the WPF library
dotnet pack NumericInput.WPF/NumericInput.WPF.csproj --configuration Release --output publish/nuget
NumericInput.Coreproduces the platform-agnostic package.NumericInput.WPFproduces the WPF control package.- Both packages include
README.mdand use the projectPackageIdvalues defined in the.csprojfiles.
Test coverage
| File | Tests | What is covered |
|---|---|---|
NumericInputTests.cs |
~300 | Core parsing, conversion, formatting, evaluator |
CultureParseTests.cs |
73 | it-IT, en-US, de-DE, InvariantCulture, round-trips, regressions |
CultureExtendedTests.cs |
413 | 32 cultures: all EU + ja-JP, zh-CN, ms-MY, en-IN, hi-IN |
Adding a new unit
Open NumericInput.Core/UnitRegistry.cs and add a line in BuildAll():
// Syntax:
// U("symbol", "Name", PhysicsFamily, factor, offset, "alias1", "alias2", …)
// where: base_value = value × factor + offset
// Example: kgf/cm² (1 kgf/cm² = 98 066.5 Pa)
U("kgf/cm²", "Kilogram-force per cm²", PhysicsFamily.Pressure, 98066.5, 0,
"at", "kgf/cm2"),
The unit is immediately available in the unit drop-down of every
NumericInputControl with PhysicsFamily = PhysicsFamily.Pressure.
…without editing this library
UnitRegistry.Register adds a unit at run time, so an application can carry
the units its own domain needs without forking the catalogue:
UnitRegistry.Register(
new UnitDefinition("kgf/cm²", "Kilogram-force per cm²",
PhysicsFamily.Pressure, 98066.5, 0, new[] { "at", "kgf/cm2" }));
// TryRegister reports why instead of throwing.
if (!UnitRegistry.TryRegister(unit, out string error))
Console.WriteLine(error);
An alias already taken within the same family is refused; one that another
family uses is fine, which is how MPa can be both a pressure and a stress.
Registration is thread-safe and readers are never blocked.
Unit systems
A UnitSystem names one unit per family — the unit an application stores and
displays a quantity in unless something says otherwise. It replaces spelling
the unit out on every property.
UnitSystem.Current = UnitSystem.EngineeringSI; // mm, MPa, N·mm, Δ°C
if (UnitSystem.Current.TryGetUnit(PhysicsFamily.Stress, out var unit))
control.DisplayUoMSymbol = unit.Symbol; // "MPa"
Three systems ship with the library — UnitSystem.SI, UnitSystem.EngineeringSI
and UnitSystem.UsCustomary — and any of them can be used as the base for a
project convention:
var house = UnitSystem.EngineeringSI
.WithName("House standard")
.With(PhysicsFamily.Pressure, "bar")
.With(PhysicsFamily.MassFlow, "t/h");
var wrong = house.Unresolvable(); // families whose symbol does not resolve
Lookups are family-scoped, so a system naming MPa for Stress resolves the
stress megapascal rather than the pressure one that shares the symbol. A system
need not cover every family; TryGetUnit simply returns false for one it does
not name.
Known limitations
| Limitation | Affected cultures | Workaround |
|---|---|---|
| NBSP thousands separator not parseable | fr-FR sv-SE fi-FI nb-NO da-DK nl-BE et-EE lt-LT lv-LV |
GroupDigits="False" or StringFormat="F2" |
| Apostrophe thousands separator not parseable | de-CH fr-CH it-CH |
GroupDigits="False" or type without separator |
| Indian 2-2-3 digit grouping not recognised | en-IN hi-IN |
Type without thousands separator or use 3-digit grouping |
| Touch keyboard | All | ShowTouchKeyboardOnTouchEnter not implemented |
| SpinnerDecorator | All | Spinners are built into the control — no separate decorator |
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on NumericInput.Core:
| Package | Downloads |
|---|---|
|
NumericInput.WPF
Production-ready WPF UserControl for engineering and scientific numeric input. KEY FEATURES • Two-way BindTo(obj, propertyName) — INotifyPropertyChanged aware, zero reflection at runtime (compiled Expression delegates), weak-event subscription (no memory leaks in virtualized panels). • Physical unit conversion — inline parsing of "100 bar", "−12.5 °C", "3.5e3 kN"; click the UoM label to switch units from a drop-down; conversion is automatic and lossless. • Expression evaluator — prefix with "=" to enter formulas: =sqrt(2)*50, =sin(pi/4), =100/3. • Rich formatting — StringFormat ("F2", "N3", "E4", "P0", custom patterns), DecimalDigits (positive = decimal places; negative = rounding magnitude), GroupDigits, Culture per control. • Full validation pipeline — NumberStyles flags, RegexPattern, MinValue/MaxValue bounds, CanValueBeNull (double?), ValidationTrigger (LostFocus or PropertyChanged/keystroke). • Spinner buttons — AllowSpinners, configurable Increment, SpinUpdateMode (PropertyChanged or AsBinding), IncrementCommand / DecrementCommand for keyboard shortcuts. • UX polish — MoveFocusOnEnter (default true), SelectAllOnFocus / SelectAllOnClick / SelectAllOnDoubleClick, configurable InputBackground and ActiveBackground (light-blue focus highlight out of the box). • NumericBox static class — inheritable attached DPs (Culture, StringFormat, DecimalDigits, AllowSpinners, MoveFocusOnEnter, SelectAll*, ValidationTrigger, SpinUpdateMode) that propagate from any container to all descendant controls. • [UserInterface] attribute — auto-populates Description, UoM, PhysicsFamily, DecimalPlaces, MinValue, MaxValue, IsReadOnly, TooltipText from the ViewModel property declaration. • Resizable TextBox column (drag thumb), fixed Description / UoM / error columns, IsTextWidthLocked DP. • Error icon with popup — shown only on validation failure; Ctrl+C copies the raw invariant value. REQUIREMENTS: .NET 8.0-windows or .NET 10.0-windows, WPF. Automatically installs NumericInput.Core (units, conversion, expression evaluator — no separate install needed). |
GitHub repositories
This package is not used by any popular GitHub repositories.