DataGrid inline edit with ValidationMessage – resolved

I'm strugling to implement FluentValidations while inline editing DataGrid... Inline editing works fine as does validations... but i never see validation messages, if it's not valid i just can't (correctly) save.

<RadzenDataGridColumn TItem="T" Property="Name" Title="Name">
	<EditTemplate Context="x">
		<RadzenTextBox @bind-Value="@x.Name" />
		<ValidationMessage For="() => x.Name" />
	</EditTemplate>
</RadzenDataGridColumn>
private async Task InlineEditEnter(T model)
{
	inlineEditContext				= new(model);
	inlineEditContextMessageStore	= new(inlineEditContext);
	Type typeVal					= typeof(IValidator<>).MakeGenericType(typeof(T));
	IValidator? validator			= serviceProvider.GetService(typeVal) as IValidator;

	if(validator is not null)
		inlineEditContext.OnValidationRequested += async (sender, args) => {
			inlineEditContextMessageStore.Clear();
			ValidationResult validationResult = await validator.ValidateAsync(new ValidationContext<T>(model));

			foreach(ValidationFailure error in validationResult.Errors)
			{
				FieldIdentifier field = new FieldIdentifier(model, error.PropertyName);
				inlineEditContextMessageStore.Add(field, error.ErrorMessage);
			}

			inlineEditContext.NotifyValidationStateChanged();
		};

	await grid!.EditRow(model);
}

private void InlineEditCancel(T model) =>
	grid!.CancelEditRow(model);

private async Task InlineEditSave(T model)
{
	await inlineEditContext!.ValidateAsync();
	if(inlineEditContext!.GetValidationMessages().Any())
		return;

	if(InlineSaveAction is not null)
		await InlineSaveAction(model);

	await grid!.UpdateRow(model);
}

I can enter the inline edit mode, edit values, if it's valid i can save and the changes are persisted. But if the model state is not valid according to FluentValidations validator, then i can't save, which is good, but the validation messages are not shown. I think my dynamic EditContext is to blame... I know that Radzen's example is using custom Radzen validator components, but is there any way to implement it with standard components like ValidationMessage?

You might need to place the validator component inside the EditTemplate:

<EditTemplate Context="x">
    <RadzenTextBox @bind-Value="x.Name" />
    <FluentValidationValidator />
    <ValidationMessage For="() => x.Name" />
</EditTemplate>

I’m using Blazilla nuget package in typical dialog setting like:

<EditForm Model="@model" OnSubmit="@Save">
	<FluentValidator AsyncMode />
	...
</EditForm>

and just placing <FluentValidator AsyncMode /> inside the EditTemplate totally solved it, thank you @enchev !

@enchev Well… It sort of works… Problem is the <FluentValidator /> (and i presume <FluentValidationValidator />) are working with EditContext somehow. And in inline edit mode, there are many edit contexts (for each model element)? Because for this setup:

<RadzenDataGridColumn TItem="T" Property="Name" Title="Name">
	<EditTemplate Context="x">
		<FluentValidator AsyncMode />
		<RadzenTextBox @bind-Value="@x.Name" />
		<ValidationMessage For="() => x.Name" />
	</EditTemplate>
</RadzenDataGridColumn>

<RadzenDataGridColumn TItem="T" Property="NameEN" Title="Name (EN)">
	<EditTemplate Context="x">
		<FluentValidator AsyncMode />
		<RadzenTextBox @bind-Value="@x.NameEN" />
		<ValidationMessage For="() => x.NameEN" />
	</EditTemplate>
</RadzenDataGridColumn>

When there are 2 validation errors one for Name and one for NameEN they are both displayed under Name and none under NameEN. If the second <FluentValidator AsyncMode /> is removed, only one error is displayed under Name. If the first <FluentValidator AsyncMode /> is removed, again one error also under Name curiously.

I’m afraid that I don’t know how this validator works and I’m unable to provide further details.

So I made it work with the intensive Claude probing :smiling_face:... I'll post it here, maybe someone will find it useful.

I have a grid component, that wraps RadzenDataGrid and is doing some other grid related stuff. So majority of these changes are made there.

  1. You'll need another UI-less component GridCaptureInlineEditContext, that will capture EditContext which is Radzen cascading to the inline edited values:
@using Microsoft.AspNetCore.Components.Forms
@namespace Your.Components.Namespace

@code {
	[CascadingParameter] private EditContext? CascadedEditContext	{ get; set; }
	[Parameter] public EventCallback<EditContext> OnCapture			{ get; set; }

	private EditContext? previousCascadedEditContext;

	protected override async Task OnParametersSetAsync()
	{
		if(CascadedEditContext is not null && previousCascadedEditContext != CascadedEditContext)
		{
			previousCascadedEditContext = CascadedEditContext;
			await OnCapture.InvokeAsync(CascadedEditContext);
		}
	}
}
  1. My shortened RadzenDataGrid wrapper Grid with only relevant bits:
@namespace Your.Components.Namespace
@typeparam T where T : IGridRowDTO
@inject IServiceProvider serviceProvider


<RadzenDataGrid
	@ref="@grid"
	TItem="T"
	SelectionMode="@DataGridSelectionMode.Single"
	...
>
	<Columns>
		@MyColumns

		<RadzenDataGridColumn
			TItem="T"
			Title="Operations"
			Frozen
			FrozenPosition="@FrozenColumnPosition.Right"
			Filterable="@false"
			Sortable="@false"
		>
			<Template Context="model">
				<div class="d-flex flex-row align-items-center gap-1">
					@if(model.Editable) {
						<RadzenButton
							Style="@ButtonStyle.Secondary"
							Title="Edit"
							Click="@(_ => InlineSave is null ? OnDetailAsync(model, true) : InlineEditEnterEditAsync(model))"
						/>
					}

					@if(model.Deletable) {
						<RadzenButton
							Style="@ButtonStyle.Danger"
							Title="Delete"
							Click="@(_ => OnDeleteAsync(model))"
						/>
					}
				</div>
			</Template>

			<EditTemplate Context="model">
				<GridCaptureInlineEditContext OnCapture="@(editContext => InlineEditOnCapureEditContext(model, editContext))" />

				<RadzenButton
					Style="@ButtonStyle.Success"
					Title="Save"
					Click="@(_ => InlineEditSave(model))"
				/>

				<RadzenButton
					Style="@ButtonStyle.Base"
					Title="Cancel"
					Click="@(_ => InlineEditCancel(model))"
				/>
			</EditTemplate>
		</RadzenDataGridColumn>
	</Columns>
</RadzenDataGrid>


@code {
	...
	[Parameter] public Func<T, bool, Task>? InlineSave { get; set; }

	...
	private bool inlineInsert;
	private EditContext? inlineEditContext;
	private ValidationMessageStore? inlineEditContextMessageStore;
	...

	private void InlineEditOnCaptureEditContext(T model, EditContext editContext)
	{
		if(inlineEditContext == editContext)
			return;

		inlineEditContext				= editContext;
		inlineEditContextMessageStore	= new(editContext);
	}

	private async Task<bool> InlineEditValidateAsync(T model)
	{
		if(inlineEditContext is null || inlineEditContextMessageStore is null)
			return true;

		Type validatorType		= typeof(IValidator<>).MakeGenericType(typeof(T));
		IValidator? validator	= serviceProvider.GetService(validatorType) as IValidator;

		inlineEditContextMessageStore!.Clear();

		if(validator is not null)
		{
			ValidationResult validationResult = await validator.ValidateAsync(new ValidationContext<T>(model));

			foreach(ValidationFailure error in validationResult.Errors)
				inlineEditContextMessageStore.Add(new FieldIdentifier(model, error.PropertyName), error.ErrorMessage);

			inlineEditContext.NotifyValidationStateChanged();
			return !validationResult.Errors.Any();
		}

		inlineEditContext.NotifyValidationStateChanged();
		return true;
	}

	public async Task InlineEditEnterInsertAsync(T model)
	{
		inlineInsert = true;
		await grid!.InsertRow(model);
	}

	private async Task InlineEditEnterEditAsync(T model)
	{
		inlineInsert = false;
		await grid!.EditRow(model);
	}

	private async Task InlineEditSave(T model)
	{
		if(!await InlineEditValidateAsync(model))
			return;

		if(InlineSave is not null)
			await InlineSave(model, inlineInsert);

		await grid!.UpdateRow(model);
		if(inlineInsert)
			await grid.Reload();
	}

	private async Task InlineEditCancel(T model)
	{
		grid!.CancelEditRow(model);
		await grid.Reload();
	}
}
  1. When you use wrapped grid component the thing that makes edit inline is that you hook InlineSave function and specify columns EditTemplate, else standard page / dialog is used.
<Grid
	@ref="@grid"
	T="T"
	InlineSave="@OnSave"
>
	<MyColumns>
		<RadzenDataGridColumn TItem="T" Property="Name" Title="Name">
			<EditTemplate Context="x">
				<RadzenTextBox @bind-Value="@x.Name" />
				<ValidationMessage For="() => x.Name" />
			</EditTemplate>
		</RadzenDataGridColumn>
		<RadzenDataGridColumn TItem="T" Property="NameEN" Title="Name (EN)">
			<EditTemplate Context="x">
				<RadzenTextBox @bind-Value="@x.NameEN" />
				<ValidationMessage For="() => x.NameEN" />
			</EditTemplate>
		</RadzenDataGridColumn>
	</MyColumns>
</Seznam>


@code {
	...

	private async Task OnSave(T row, bool isInsert)
	{
		// your local model or db save...
	}

	// you'll hook up this method to some button
	public Task OnInsert(T model) =>
		grid?.InlineEditEnterInsertAsync(model) ?? Task.CompletedTask;
}