BootstrapBlazor: Validation error inside a Table always appears on the first row

3 weeks ago 31
ARTICLE AD BOX

I am using BootstrapBlazor with a Table component inside a ValidateForm. I am binding the form directly to a List<T>.

Problem: my custom validation logic (IsValid in C#) works correctly and detects errors in specific rows. However, the UI feedback behaves strangely:

Wrong target: if I have an error in row 3, the red border and error message appear on row 1

Phantom dependency: it acts as if all inputs in the table are extensions of the first row. The EditContext seems to map the validation message to the first instance found.

Broken context: if I delete the first row, validation stops working entirely for the remaining rows.

My code - Razor view:

<ValidateForm Model="@PeticionPagosMasivos" DisableAutoSubmitFormByEnter="true" OnValidSubmit="ButtonRealizarPagosMasivos_ValidSubmit"> @if (EstaCargandoListadoPagosMasivos) { <SkeletonTable Columns="1" Rows="5" /> } <div class="row mb-2"> <div class="col-12"> <div class="table-container"> <Table HeaderStyle="TableHeaderStyle.Dark" Items="@PeticionPagosMasivos.PagosCuentasPagarProveedor" IsBordered="true" AllowDragColumn="true" AllowResizing="true" IsFixedHeader="true" IsAutoRefresh="true" ShowLoadingInFirstRender="false" ShowColumnWidthTooltip="true" ShowFooter="true" IsHideFooterWhenNoData="true" TableSize="TableSize.Compact"> <TableColumns> <TableColumn Text="@LocalResources!["TextoObservacion"]" @bind-Field="@context.Observacion" Sortable="true" Align="Alignment.Center" /> <TableColumn Text="@LocalResources!["TextoNumeroFactura"]" @bind-Field="@context.NumeroFactura" Sortable="true" Align="Alignment.Center" /> <TableColumn Text="@LocalResources!["TextoCuentaOrigen"]" @bind-Field="@context.NombreCuentaOrigen" Sortable="true" Align="Alignment.Center" /> <TableColumn Text="@LocalResources!["TextoValorPago"]" @bind-Field="@context.ValorPago" Sortable="true" Align="Alignment.Center" Width="200"> <Template Context="value"> <BootstrapInput @bind-Value="value.Row.ValorPago" Id="@($"valorpago-{value.Row.CuentaPagarProveedorId}")" ShowLabel="false" ShowRequired="true" RequiredErrorMessage="@GlobalResources!["TextoCampoObligatorio"]" FormatString="@WebAppUtilities.InputDecimalNumberFormat" /> </Template> </TableColumn> <TableColumn Text="@LocalResources!["TextoFechaPago"]" @bind-Field="@context.FechaPago" FormatString="dd/MM/yyyy" Align="Alignment.Center" Width="140" Sortable="true" /> <TableColumn Text="" @bind-Field="@context.CuentaPagarProveedorId" Align="Alignment.Center" Sortable="false" Width="64" TextWrap="true"> <Template Context="value"> <Button class="text-white" TooltipText="@GlobalResources!["TextoEliminar"]" IsAsync="true" Icon="bi bi-trash-fill" Color="Color.Danger" Size="Size.Small" OnClick="@(() => ButtonEliminarValorPago_Click(value.Row))" /> </Template> </TableColumn> </TableColumns> </Table> </div> </div> </div> <div class="d-flex justify-content-end gap-2"> <Button Text="@GlobalResources!["TextoLimpiar"]" Size="Size.Small" Icon="bi bi-eraser-fill" Color="Color.Danger" IsAsync="true" OnClick="ButtonLimpiar_Click" /> <Button Text="@GlobalResources!["TextoGuardar"]" Size="Size.Small" Icon="bi bi-floppy2-fill" Color="Color.Success" IsAsync="true" ButtonType="ButtonType.Submit" /> </div> </ValidateForm>

Model and validator:

public class PeticionListadoPagosMasivosCuentaPagarProveedor { public string? NombreProveedor { get; set; } public long? CuentaPagarProveedorId { get; set; } public string? Observacion { get; set; } public string? NumeroFactura { get; set; } public long? CuentaOrigenId { get; set; } public string? NombreCuentaOrigen { get; set; } public decimal? SaldoPendiente { get; set; } [MaxValorPago] public decimal? ValorPago { get; set; } public DateTime? FechaPago { get; set; } } public class MaxValorPagoAttribute : ValidationAttribute { public MaxValorPagoAttribute() { ErrorMessageResourceType = typeof(Resources.Pages.Financiero.CuentasPagarProveedor.PagosMasivosCuentaPagarProveedor); ErrorMessageResourceName = nameof(Resources.Pages.Financiero.CuentasPagarProveedor.PagosMasivosCuentaPagarProveedor.ErrorMessageValorPago); } protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { if (validationContext.ObjectInstance is not PeticionListadoPagosMasivosCuentaPagarProveedor row || value == null) return ValidationResult.Success; if (value is not decimal valorPago) return new ValidationResult(FormatErrorMessage(validationContext.DisplayName)); if (row.SaldoPendiente.HasValue && valorPago > row.SaldoPendiente.Value) { var mensaje = ErrorMessageString.Replace("[saldo]", row.SaldoPendiente.Value.ToString("N2", CultureInfo.CurrentCulture)); return new ValidationResult(mensaje, new string[] { validationContext.MemberName! }); } return ValidationResult.Success; } }

Here is an example of how the third row is modified by adding an extra 9 so you can see how the validation is always displayed on the first record. It's as if the other records don't actually exist, because if I delete that first record, nothing is displayed in the others. It's not that the validation is always displayed on the first record regardless of whether it's deleted and replaced by another; it's that it's always done on the first record as if it were the only one there.

I've tried everything, but I can't find the answer.

Read Entire Article