Custom sfNumericTextBox with complex type
hi,
I would like to create a custom component based on the sfNumericTextBox.
I tried following the sfTextBox documentation ( Create a custom component using Blazor TextBox | Syncfusion), but I want to go further by using an object rather than a simple TValue type.
I have a simplified ViewModel like this:
public partial class InvoiceCreatingVM : ObservableObject
{
[ObservableProperty]
private Money? montantTTC;
}
My simplified "Money" class looks like this:
public class Money : IComparable<Money>, IComparable
{
private string? _format;
public string? Format
{
get
{
if (!string.IsNullOrEmpty(_format))
return _format;
//Format was computed here
}
}
private double _value;
public double Value
{
get
{
return _value;
}
set
{
_value = value;
}
}
private string?_currency;
public string? Currency
{
get
{
return _currency ;
}
set
{
_currency = value;
}
}
}
and my EditForm:
<div class="col-3 e-label-position-top">
<MoneyTextBoxComponent @bind-Value="@InvoiceCreatingVM.MontantTTC.Value" ID="MontantTTC" PlaceHolder="Montant TTC" ValidationMessage="@(() => InvoiceCreatingVM.MontantTTTC.Value)" @bind-Value:event="ValueChanged"></MoneyTextBoxComponent>
<ValidationMessage For="@(() => InvoiceCreatingVM.MontantTTC.Value)"></ValidationMessage>
</div>
How can I adapt the files I've attached to replace the "double" type with my "Money" class?
While keeping the validation system.
Basically, I would like this in EditForm:
<div class="col-3 e-label-position-top">
<MoneyTextBoxComponent @bind-Value="@InvoiceCreatingVM.MontantTTC" ID="MontantTTC" PlaceHolder="Montant TTC" ValidationMessage="@(() => InvoiceCreatingVM.MontantTTC)" @bind-Value:event="ValueChanged"></MoneyTextBoxComponent>
<ValidationMessage For="@(() => InvoiceCreatingVM.MontantTTC)"></ValidationMessage>
</div>
Thanks
Geff
Hi LSE Crew,
We're pleased to share the complete implementation guide for binding a Money object with validation support in your Blazor form. This solution moves you from binding simple double values to binding complete Money objects while maintaining full EditForm validation.
We’ve created a custom component based on the NumericTextBox that works with complex types while preserving validation. Here's the complete implementation:
1. The Money Class (Money.cs)
Your Money class is perfectly structured with data validation:
public class Money : IComparable<Money>, IComparable { private string? _format; public string? Format { get => _format ?? "C2"; // Default currency format set => _format = value; }
[Range(0.01, double.MaxValue, ErrorMessage = "Amount must be greater than zero.")] public double Value { get; set; }
public string? Currency { get; set; }
public int CompareTo(Money? other) => Value.CompareTo(other?.Value); public int CompareTo(object? obj) => obj is Money money ? CompareTo(money) : -1; } |
Key Points:
- The [Range] attribute validates that the Value property is always positive
- The Format property defaults to "C2" (currency format with 2 decimals)
- The IComparable interfaces allow Money objects to be compared by their Value
2. The MoneyTextBoxComponent (Complete Implementation)
This is a generic component constrained to Money objects. It transparently maps between the double? input used by SfNumericTextBox and your Money object:
Markup Section:
@typeparam TValue where TValue : Money @using Syncfusion.Blazor.Inputs @using System.Linq.Expressions
<div class="e-numeric-input"> <SfNumericTextBox TValue="double?" @bind-Value="@InternalNumericValue" ID="@ID" Placeholder="@PlaceHolder" FloatLabelType="@FloatLabelType" Enabled="@Enabled" ShowSpinButton="@ShowSpinButton" Format="@InternalFormat" ShowClearButton="@ShowClearButton" CssClass="@CssClass"> <NumericTextBoxEvents TValue="double?" ValueChange="@OnValueChanged"></NumericTextBoxEvents> </SfNumericTextBox> </div> |
Code-Behind Section:
@code { [Parameter] public TValue Value { get; set; } [Parameter] public EventCallback<TValue> ValueChanged { get; set; } [Parameter] public Expression<Func<TValue>> ValueExpression { get; set; } [Parameter] public string ID { get; set; } [Parameter] public string PlaceHolder { get; set; } [Parameter] public FloatLabelType FloatLabelType { get; set; } = FloatLabelType.Auto; [Parameter] public bool Enabled { get; set; } = true; [Parameter] public bool ShowSpinButton { get; set; } = true; [Parameter] public bool ShowClearButton { get; set; } = true; [Parameter] public string CssClass { get; set; } [Parameter] public RenderFragment ValidationMessage { get; set; }
private double? _internalValue;
protected override void OnParametersSet() { _internalValue = Value?.Value; }
private double? InternalNumericValue { get => _internalValue; set => _internalValue = value; }
private string InternalFormat => Value?.Format ?? "N2";
private async Task OnValueChanged(Syncfusion.Blazor.Inputs.ChangeEventArgs<double?> args) { _internalValue = args.Value;
// Create a new Money instance if it doesn't exist if (Value == null && args.Value.HasValue) { Value = (TValue)Activator.CreateInstance(typeof(TValue)); }
// Update the Money object's Value property and notify parent component if (Value != null) { Value.Value = args.Value ?? 0; await ValueChanged.InvokeAsync(Value); } } } |
Key Implementation Details:
Feature | Explanation |
Generic Type Constraint | where TValue : Money ensures only Money-compatible types can be used |
Internal Value Mapping | _internalValue holds the double? value for NumericTextBox, preventing infinite rendering loops |
OnParametersSet | Initializes internal value when the Money parameter is set |
OnValueChanged Event | Handles bidirectional binding by updating the Money object's Value and invoking the ValueChanged callback |
InternalFormat Property | Dynamically returns the Money object's Format property for currency display |
3. How to Use It in Your EditForm
Now you can bind directly to the Money object as you wanted:
@page "/" @using System.ComponentModel.DataAnnotations @rendermode InteractiveServer
<EditForm Model="@InvoiceCreatingVM1" OnValidSubmit="HandleValidSubmit"> <DataAnnotationsValidator />
<div class="col-3 e-label-position-top"> <MoneyTextBoxComponent @bind-Value="@InvoiceCreatingVM1.MontantTTC" ID="MontantTTC" PlaceHolder="Montant TTC"> </MoneyTextBoxComponent>
<!-- Validation now works with the Money object --> <ValidationMessage For="@(() => InvoiceCreatingVM1.MontantTTC.Value)" /> </div>
<button type="submit" class="btn btn-primary">Submit</button> </EditForm>
@code { private InvoiceCreatingVM InvoiceCreatingVM1 { get; set; } = new InvoiceCreatingVM();
private void HandleValidSubmit() { // Your Money object is fully populated Console.WriteLine($"Submitted amount: {InvoiceCreatingVM1.MontantTTC.Value} {InvoiceCreatingVM1.MontantTTC.Currency}"); }
public class InvoiceCreatingVM { [Required(ErrorMessage = "Montant TTC is required.")] public Money MontantTTC { get; set; } = new Money(); } } |
How Validation Works
- EditForm Validation - The [Required] attribute ensures the Money object exists
- Money Value Validation - The [Range] attribute validates that the amount is greater than zero
- Real-time Feedback - The <ValidationMessage> component displays errors directly from the Money object's Value property
For your reference, we’ve also included a sample showing this in action along with a GIF demonstration:
Gif:
Sample: https://www.syncfusion.com/downloads/support/directtrac/general/ze/TextBoxsamp
hi,
Thanks for your help.
Unless I'm mistaken, it seems your sample isn't working.
The `<ValidationMessage>` element isn't displaying under the `<MoneyTextBoxComponent>` element, as shown in the attached GIF.
Do you have an example using Fluent Validation/Blazilla?
So that I can create rules like this:
public class InvoiceCreatingValidator : AbstractValidator<InvoiceCreatingVM>
{
public InvoiceCreatingValidator()
{
RuleFor(x => x.InclusiveTaxAmount)
.Must(mny => mny != null && !mny.IsEmpty())
.WithMessage("Le montant TTC est obligatoire.");
}
}
public class Money{
public bool IsEmpty()
{
return Value == 0.00;
}
}
Therefore, in the EditForm, the `<ValidationMessage>` must be on the property of type `Money`.
I don't want any DataAnnotation in my Money class.
<div class="col-4 e-label-position-top">
<MoneyTextBoxComponent Value="@InvoiceCreatingVM.InclusiveTaxAmount" ID="InclusiveTaxAmount" PlaceHolder="Montant TTC" ShowSpinButton=false></MoneyTextBoxComponent>
<ValidationMessage For="@(() => InvoiceCreatingVM.InclusiveTaxAmount)"></ValidationMessage>
</div>
thanks
Attachment: 20260318_16h00_23_41f571fb.gif
Hi LSE Crew,
We're pleased to inform you that we have successfully implemented a FluentValidation-based solution that resolves the <ValidationMessage> display issue for your MoneyTextBoxComponent.
Below, we provide a comprehensive explanation of our implementation approach, complete with code examples and documentation references.
We have integrated FluentValidation with Blazor's EditForm component using the Blazilla validator helper. This approach allows you to:
- Define validation rules for your Money property without DataAnnotations
- Display validation messages directly under your custom MoneyTextBoxComponent
- Leverage the powerful, fluent API provided by FluentValidation
- Customize validation rules completely to match your business requirements
How It Works:
1. EditForm with FluentValidator
The EditForm component manages the validation lifecycle through an EditContext. By including the <FluentValidator /> component (from Blazilla), we hook into this context to run FluentValidation rules automatically.
2. Two-Way Data Binding
The @bind-Value directive on MoneyTextBoxComponent ensures that:
- Value changes are synchronized back to the model
- The EditContext is notified of field changes
- Validation rules run against the updated property
3. Validation Message Binding
The <ValidationMessage For="@(() => InvoiceCreatingVM.Property)" /> element displays errors for the specified property, but only when:
- The property is bound with @bind-Value (not one-way Value="...")
- The validator rule targets the same property
- A validation error exists for that property
Implementation Steps:
Step 1: Create Your Money Class
public class Money { public double Value { get; set; }
public bool IsEmpty() { return Value == 0.00; } } |
Step 2: Create Your ViewModel
public class InvoiceCreatingVM { public Money MontantTTC { get; set; } = new(); } |
Step 3: Create Your Validator (FluentValidation)
using FluentValidation;
public class InvoiceCreatingVMValidator : AbstractValidator<InvoiceCreatingVM> { public InvoiceCreatingVMValidator() { RuleFor(x => x.MontantTTC) .Must(mny => mny != null && !mny.IsEmpty()) .WithMessage("Le montant TTC est obligatoire."); } } |
Key Points:
- No DataAnnotations needed on the Money class
- The validator uses Must() to check the custom IsEmpty() method
- Error message is fully customizable
Step 4: Register the Validator in Program.cs
builder.Services.AddTransient<IValidator<InvoiceCreatingVM>, InvoiceCreatingVMValidator>(); |
Step 5: Use in Your Razor Component
@page "/" @using Blazilla @using Syncfusion.Blazor.Buttons @using FluentValidation @rendermode InteractiveServer
<EditForm Model="@InvoiceCreatingVM1" OnValidSubmit="HandleValidSubmit"> <FluentValidator /> <ValidationSummary />
<div class="col-4 e-label-position-top"> <!-- Two-way binding is CRITICAL --> <MoneyTextBoxComponent @bind-Value="@InvoiceCreatingVM1.MontantTTC" ID="MontantTTC" PlaceHolder="Montant TTC" ShowSpinButton="false" /> </div>
<SfButton type="submit" class="btn btn-primary">Submit</SfButton> </EditForm>
@code { private void HandleValidSubmit() { Console.WriteLine($"✅ Submitted amount: {InvoiceCreatingVM1.MontantTTC.Value}"); }
private InvoiceCreatingVM InvoiceCreatingVM1 = new(); } |
FluentValidation provides a powerful, chainable API that allows you to customize validation rules completely you can Customizing Validation as Per Your Requirements.
Reference Documentation:
- Blazilla (NuGet Package): NuGet Gallery | Blazilla 2.3.0
For your reference, we’ve also included a sample showing this in action along with a GIF demonstration:
Gif:
Sample: https://www.syncfusion.com/downloads/support/directtrac/general/ze/TextBosSampl
Thank you for this new response.
By putting
<ValidationMessage For="@(() => InvoiceCreatingVM1.MontantTTC)"/>
under the <MoneyTextBoxComponent>, I get what I want, but what I'm really trying to achieve is the CSS style that should be applied to the SfNumericTextBox as if it were used directly in the EditForm.
Normally, the border and the placeholder turn red.
This isn't happening here; the CSS style isn't being applied.
Like this
Hi LSE Crew,
We understand that while the <ValidationMessage> component displays the error text correctly, your main goal is to have the NumericTextBox itself styled with the red border and placeholder, just as it would when used directly inside an EditForm
The key challenge is that Syncfusion’s NumericTextBox doesn’t automatically apply Blazor’s validation styling when wrapped in a custom component. To bridge this gap, we need to ensure two things:
- The is-invalid class is applied to the correct element.
- The CSS selector targets the actual rendered <input> inside the Syncfusion control.
Below are the complete code changes that make this work:
MoneyTextBoxComponent.razor
- Added @inherits InputBase<TValue> - This integrates the component with Blazor's EditContext for validation
- Added @using Microsoft.AspNetCore.Components.Forms - Required namespace for validation support
@typeparam TValue where TValue : Money @using Syncfusion.Blazor.Inputs @using System.Linq.Expressions @using Microsoft.AspNetCore.Components.Forms @inherits InputBase<TValue> <!-- ADDED --> |
Validation Detection Method:
- We added a method to automatically apply the is-invalid class when validation fails:
private string GetCssClass() { var classes = CssClass ?? ""; if (EditContext != null && !FieldIdentifier.Equals(default(FieldIdentifier))) { if (EditContext.GetValidationMessages(FieldIdentifier).Any()) { classes += " is-invalid"; // Applied when field has validation errors } } return classes; } |
Updated NumericTextBox:
<SfNumericTextBox TValue="double?" @bind-Value="@InternalNumericValue" ID="@ID" Placeholder="@PlaceHolder" FloatLabelType="@FloatLabelType" Enabled="@Enabled" ShowSpinButton="@ShowSpinButton" Format="@InternalFormat" ShowClearButton="@ShowClearButton" CssClass="@GetCssClass()"> <!-- CHANGED: Now calls GetCssClass() --> <NumericTextBoxEvents TValue="double?" ValueChange="@OnValueChanged"></NumericTextBoxEvents> </SfNumericTextBox> |
wwwroot/styles.css:
- We added CSS rules to style the invalid state. Add this code to your CSS file:
.is-invalid { border-color: red !important; } .is-invalid .e-float-text.e-label-bottom { color: red !important; } .e-numeric.is-invalid .e-float-text { color: red !important; } |
How It Works
- When validation fails, EditContext detects the error.
- GetCssClass() adds the is-invalid class to the NumericTextBox.
- The CSS rules now cover:
- The outer wrapper (.is-invalid)
- The floating label (.e-float-text)
- The actual input element inside Syncfusion’s markup (.e-input-group input)
- This ensures both the border and the placeholder/floating label text turn red, matching the native Blazor validation experience.
For your reference, we’ve also included a sample showing this in action along with a GIF demonstration:
Gif:
Sample: https://www.syncfusion.com/downloads/support/directtrac/general/ze/Sampl2
Perfect, I'm getting what I want:
I had to make a few adjustments because some components are used in "ReadOnly" mode (Value, not @bind-value), so ValueExpression was null, but I worked around it in SetParametersAsyn() (I don't know if it's the right method, but it works).
public override Task SetParametersAsync(ParameterView parameters) { parameters.SetParameterProperties(this); // applique les paramètres sur this if (ValueExpression is null) { // Si pas d'expression, on forge une expression à partir de la valeur actuelle // Cela permet à InputBase de fonctionner même sans expression fournie (par le bind-value normalement) var capturedValue = Value; ValueExpression = () => capturedValue!; // forge l'expression AVANT InputBase _hasFieldIdentifier = false; } else _hasFieldIdentifier = true; return base.SetParametersAsync(ParameterView.Empty); // Empty car déjà appliqués } |
And I handled the OnBlur event to re-run the validation if it failed.
private Task OnBlur(NumericBlurEventArgs<double?> args) { // Notify validation also on blur (like built-in inputs) if (EditContext is not null && _hasFieldIdentifier) { EditContext.NotifyFieldChanged(FieldIdentifier); } return Task.CompletedTask; } |
Thanks for ypur help.
Hi LSE crew,
Thank you for the update. Please get back to us for assistance in the future.
Regards,
Shereen
- 7 Replies
- 3 Participants
- Marked answer
-
LC LSE Crew
- Mar 16, 2026 08:29 AM UTC
- Mar 25, 2026 12:57 PM UTC