I am using MudBlazor form components to create a form. The form has some static components and some dynamic components. I am having trouble getting the dynamic components to validate.
Form
<MudPaper Class="px-4 pb-2 mt-5 rounded-lg"><MudForm @ref="_form" Validation="@(AddHeartbeatSettingViewModelValidator.ValidateValue)" Model="@AddHeartbeatSettingViewModel" @bind-Errors="@_errors"><MudGrid> @if (HasGeneralErrors()) {<MudItem xs="12"><MudAlert Severity="Severity.Error" Variant="Variant.Filled" Class="mb-4"> There was a problem with your submission<ul> @foreach (var error in _generalErrors) {<li>@error.ErrorMessage</li> }</ul></MudAlert></MudItem> }<MudItem xs="12"><MudSwitch T="bool" @bind-Value="AddHeartbeatSettingViewModel.IsEnabled" Label="Enabled" Color="Color.Primary"/></MudItem><MudItem xs="12"><FrequencySelector ForFrequency="() => AddHeartbeatSettingViewModel.Frequency.Value" ForTimeUnit="() => AddHeartbeatSettingViewModel.Frequency.TimeUnit" Disabled="@(!AddHeartbeatSettingViewModel.IsEnabled)" @bind-Frequency="@AddHeartbeatSettingViewModel.Frequency.Value" @bind-TimeUnit="@AddHeartbeatSettingViewModel.Frequency.TimeUnit"/></MudItem><MudItem xs="12"><MudText Typo="Typo.h5">Pester</MudText> @if (AddHeartbeatSettingViewModel.Pester.Intervals.Count > 0) { for (var i = 0; i < AddHeartbeatSettingViewModel.Pester.Intervals.Count; i++) { var localCount = i;<FrequencySelector ForFrequency="() => AddHeartbeatSettingViewModel.Pester.Intervals[localCount].Value" ForTimeUnit="() => AddHeartbeatSettingViewModel.Pester.Intervals[localCount].TimeUnit" Disabled="@(!AddHeartbeatSettingViewModel.IsEnabled)" @bind-Frequency="@AddHeartbeatSettingViewModel.Pester.Intervals[localCount].Value" @bind-TimeUnit="@AddHeartbeatSettingViewModel.Pester.Intervals[localCount].TimeUnit"/> } }<MudButton Disabled="@(!AddHeartbeatSettingViewModel.IsEnabled)" StartIcon="@Icons.Material.Filled.AddCircle" Color="AddPesterButtonColor" Variant="Variant.Filled" Class="rounded-lg flex-initial" OnClick="@(() => HandleAddPester())">@_addPesterButtonLabel</MudButton></MudItem><MudItem xs="12"><MudButton StartIcon="@Icons.Material.Filled.AddCircle" Disabled="IsSaving" Color="SubmitButtonColor" Variant="Variant.Filled" Class="rounded-lg flex-initial" OnClick="@(() => HandleSubmit())"> @_submitButtonLabel</MudButton></MudItem></MudGrid></MudForm></MudPaper>Logic
public partial class AddHeartbeatSetting : AbstractAuthRequiredComponent{ [Inject] private AddHeartbeatSettingViewModelValidator AddHeartbeatSettingViewModelValidator { get; set; } = null!; private const Color SubmitButtonColor = Color.Primary; private const Color AddPesterButtonColor = Color.Secondary; private MudForm _form = null!; private string[] _errors = []; private List<ValidationFailure> _generalErrors = []; protected readonly AddHeartbeatSettingViewModel AddHeartbeatSettingViewModel = new(); private bool IsSaving; private string _submitButtonLabel = "Create Heartbeat Setting"; private string _addPesterButtonLabel = "Add Interval"; protected override void ProfileAvailable(Profile profile) { base.ProfileAvailable(profile); AddHeartbeatSettingViewModel.ProfileId = profile.Id; } private bool HasGeneralErrors() => _generalErrors.Count > 0; private async Task HandleSubmit() { await _form.Validate(); var validationResult = await AddHeartbeatSettingViewModelValidator.ValidateAsync(AddHeartbeatSettingViewModel); _generalErrors = validationResult.Errors.FindAll(error => error.PropertyName == string.Empty); } private void HandleAddPester() { AddHeartbeatSettingViewModel.Pester.Intervals.Add(new FrequencyViewModel()); }}View Model
public class AddHeartbeatSettingViewModel{ public ProfileId? ProfileId { get; set; } public bool IsEnabled { get; set; } public FrequencyViewModel Frequency { get; set; } = new(); public PesterViewModel Pester { get; set; } = new();}public class FrequencyViewModel{ public int Value { get; set; } public int TimeUnit { get; set; }}public class PesterViewModel{ public List<FrequencyViewModel> Intervals { get; set; } = [];}Validator
public class AddHeartbeatSettingViewModelValidator : BlazorFormValidator<AddHeartbeatSettingViewModel>{ public AddHeartbeatSettingViewModelValidator(IValidator<FrequencyViewModel> frequencyViewModelValidator) { RuleFor(x => x.ProfileId) .NotNull() .WithMessage("ProfileId is required.") .Must(BeValidGuid) .WithMessage("Invalid ProfileId."); When(x => x.IsEnabled, () => { RuleFor(x => x.Frequency) .SetValidator(frequencyViewModelValidator); RuleForEach(x => x.Pester.Intervals) .SetValidator(frequencyViewModelValidator); }); } private static bool BeValidGuid(ProfileId? guid) => guid?.Value != Guid.Empty;}FrequencySelector Component
Razor
<div class="d-flex flex-row align-baseline"><MudNumericField T="int" Label="@_frequencyLabel" For="@ForFrequency" Disabled="@Disabled" Class="mr-10" ValueChanged="UpdateFrequency"/><MudSelect T="int" Label="Time Unit:" For="@ForTimeUnit" Variant="Variant.Outlined" Disabled="@Disabled" Margin="Margin.Dense" AnchorOrigin="Origin.BottomCenter" ValueChanged="UpdateTimeUnit" Class="ml-10"> @foreach (var timeUnit in AfterLife.Domain.Heartbeats.Enums.TimeUnit.List) {<MudSelectItem T="int" Value="@timeUnit.Value"> @timeUnit.Name</MudSelectItem> }</MudSelect></div>Logic
public partial class FrequencySelector : ComponentBase{ /// <summary> /// The frequency /// <remarks>REQUIRED</remarks> /// </summary> [Parameter, EditorRequired] public int Frequency { get; set; } /// <summary> /// The frequency changed event /// </summary> [Parameter] public EventCallback<int> FrequencyChanged { get; set; } /// <summary> /// The time unit /// <remarks>REQUIRED</remarks> /// </summary> [Parameter, EditorRequired] public int TimeUnit { get; set; } /// <summary> /// The time unit changed event /// </summary> [Parameter] public EventCallback<int> TimeUnitChanged { get; set; } /// <summary> /// The disabled state for the component /// </summary> [Parameter] public bool Disabled { get; set; } /// <summary> /// The model field representing validation results for the frequency property /// </summary> [Parameter] public Expression<Func<int>>? ForFrequency { get; set; } /// <summary> /// The model field representing validation results for the time unit property /// </summary> [Parameter] public Expression<Func<int>>? ForTimeUnit { get; set; } private string _frequencyLabel = AfterLife.Domain.Heartbeats.Enums.TimeUnit.FromValue(0).ToString(); private const int MinFrequency = 0; private async Task UpdateFrequency(int value) { Frequency = value; await FrequencyChanged.InvokeAsync(Frequency); } private async Task UpdateTimeUnit(int value) { if (!AfterLife.Domain.Heartbeats.Enums.TimeUnit.TryFromValue(value, out var timeUnit)) return; TimeUnit = timeUnit; _frequencyLabel = timeUnit.ToString(); await TimeUnitChanged.InvokeAsync(TimeUnit); }}This renders as expected and var validationResult = await AddHeartbeatSettingViewModelValidator.ValidateAsync(AddHeartbeatSettingViewModel); does capture all of the validation errors as expected. However, the dynamically added FrequencySelector components do not display error messages in-line when being edited or when await _form.Validate(); is called. The FrequencySelector component that is there initially when the form is rendered does behave as expected.
What am I doing wrong?