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


7 Replies 1 reply marked as answer

KN Kundurthi Naga Siddartha Kundurthi Vennela Prasad Syncfusion Team March 18, 2026 02:18 PM UTC

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

 

 



LC LSE Crew March 18, 2026 03:13 PM UTC

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


KN Kundurthi Naga Siddartha Kundurthi Vennela Prasad Syncfusion Team March 19, 2026 02:54 PM UTC

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:

 

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

 



LC LSE Crew March 19, 2026 03:35 PM UTC

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 
Image_7031_1773934536042



KN Kundurthi Naga Siddartha Kundurthi Vennela Prasad Syncfusion Team March 24, 2026 04:00 PM UTC

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:


Samplehttps://www.syncfusion.com/downloads/support/directtrac/general/ze/Sampl2

 


Marked as answer

LC LSE Crew March 25, 2026 10:05 AM UTC

Perfect, I'm getting what I want:
Image_7950_1774433076543

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.



SS Shereen Shajahan Syncfusion Team March 25, 2026 12:57 PM UTC

Hi LSE crew,

Thank you for the update. Please get back to us for assistance in the future.

Regards,

Shereen


Loader.
Up arrow icon