Validation without @bind-Value

I have a custom getter/setter for values - is it possible to use the built-in Validation components without @bind-Value?

Here's an example where the form is always valid, even though it should be displaying errors for a blank Username.

Is there any way around this? Eg somehow telling the EditContext of the RadzenTemplateForm something has changed?

@page "/validators"
@using System.ComponentModel.DataAnnotations
@using System.Text.Json


<RadzenTemplateForm TItem="Model" Data=@model Submit=@OnSubmit InvalidSubmit=@OnInvalidSubmit>
    <RadzenStack>
        <RadzenRow AlignItems="AlignItems.Center">
            <RadzenColumn Size="12" SizeMD="4">
                <RadzenLabel Text="Username" Component="Username" />
            </RadzenColumn>
            <RadzenColumn Size="12" SizeMD="8"> 
                <RadzenTextBox Name="Username" Value="@model.Username" ValueChanged="@((v) => model.Username = v)" class="rz-w-100" />
                <RadzenDataAnnotationValidator Component="Username"  />
            </RadzenColumn>
        </RadzenRow>
        <RadzenRow AlignItems="AlignItems.Center" class="rz-mt-4">
            <RadzenColumn Size="12" Offset="0" SizeMD="8" OffsetMD="4">
                <RadzenButton ButtonType="ButtonType.Submit" Text="Submit"></RadzenButton>
            </RadzenColumn>
        </RadzenRow>
    </RadzenStack>
</RadzenTemplateForm>
<RadzenText Text="@message" />
@code {
    class Model
    {
        [Required]
        [StringLength(15, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long", MinimumLength = 4)]
        public string Username { get; set; }
    }

    Model model = new Model();
    string message;

    void OnSubmit(Model model)
    {
        message = $"Submit: {JsonSerializer.Serialize(model, new JsonSerializerOptions() { WriteIndented = true })}";
    }

    void OnInvalidSubmit(FormInvalidSubmitEventArgs args)
    {
        message = $"InvalidSubmit: {JsonSerializer.Serialize(args, new JsonSerializerOptions() { WriteIndented = true })}";
    }
}

As far as we know this is not possible - that’s how Blazor validation works.

You can try setting ValueExpression. It is what Blazor validation would use (and is automatically generated when @bind-Value is used).

Perfect, ValueExpression did the job!

I'm building a dynamic form, so couldn't access the property directly - but building an expression dynamically worked!

Code for the ValueExpression.

    /// <summary>
    /// Dynamically creates an Expression<Func<TProperty>> for a given property on an object instance.
    /// This version specifically caters to converting non-nullable property types to their nullable
    /// counterparts (e.g., bool to bool?) without introducing an Expression.Convert (UnaryExpression).
    /// </summary>
    /// <typeparam name="TProperty">The expected type of the value that the expression will return (e.g., bool?).</typeparam>
    /// <param name="modelType">The actual Type of the model instance (e.g., typeof(MyTestModel)).</param>
    /// <param name="propertyName">The name of the property to create the expression for (e.g., "BoolProp").</param>
    /// <param name="instance">The actual instance of the model (e.g., new MyTestModel()).</param>
    /// <returns>An Expression<Func<TProperty>> that represents accessing the property, with implicit conversion if necessary.</returns>
    public static Expression<Func<TProperty>> CreateValueExpression<TProperty>(Type modelType, string propertyName, object instance)
    {
        if (instance == null)
        {
            throw new ArgumentNullException(nameof(instance), "Model instance cannot be null.");
        }

        // Get the PropertyInfo for the specified property name from the model's type.
        var property = modelType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
        if (property == null)
        {
            throw new ArgumentException($"Property '{propertyName}' not found on type '{modelType.FullName}'.");
        }
        if (!property.CanRead)
        {
            throw new ArgumentException($"Property '{propertyName}' on type '{modelType.FullName}' is not readable.");
        }

        // Create a constant expression representing the actual model instance.
        var instanceExpression = Expression.Constant(instance, modelType);

        // Create an expression that represents accessing the property of the instance.
        Expression propertyAccess = Expression.Property(instanceExpression, property);

        // Determine the target type for the expression (TProperty)
        Type targetType = typeof(TProperty);

        // Determine the source type from the property
        Type sourceType = property.PropertyType;

        if (sourceType == targetType)
        {
            // No conversion needed, just directly create the lambda.
            return Expression.Lambda<Func<TProperty>>(propertyAccess);
        }
        else
        {
            // If we reach here, it means the types don't match.
            // Since the goal is to *avoid* UnaryExpression (Expression.Convert),
            // we'll throw an exception here, guiding the user to ensure TProperty matches
            // the property's actual type or its nullable counterpart if applicable.
            throw new InvalidOperationException(
                $"Cannot create Expression<Func<{targetType.Name}>> for property '{propertyName}' " +
                $"of type '{sourceType.Name}'. To avoid UnaryExpression (Expression.Convert), " +
                $"ensure that TProperty ({targetType.Name}) either exactly matches the property's actual type.");
        }
    }



}