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.
<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>
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());
}
}
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; } = [];
}
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;
}
<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>
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?
It turns out that the solution was to wrap each component in the for(each) loop with a child MudForm
component.
E.g.
<MudItem xs="12">
<MudText Typo="Typo.h5">Pester</MudText>
@if (AddHeartbeatSettingViewModel.Pester.Intervals.Count > 0)
{
foreach (var frequencyModel in AddHeartbeatSettingViewModel.Pester.Intervals))
{
<MudForm Model="frequencyModel" Validation="FrequencyViewModelValidator.ValidateValue">
<FrequencySelector
ForFrequency="() => frequencyModel.Value"
ForTimeUnit="() => frequencyModel.TimeUnit"
Disabled="@(!AddHeartbeatSettingViewModel.IsEnabled)"
@bind-Frequency="@frequencyModel.Value"
@bind-TimeUnit="@frequencyModel.TimeUnit"/>
</MudForm>
}
}
<MudButton
Disabled="@(!AddHeartbeatSettingViewModel.IsEnabled)"
StartIcon="@Icons.Material.Filled.AddCircle"
Color="AddPesterButtonColor"
Variant="Variant.Filled"
Class="rounded-lg flex-initial"
OnClick="@(() => HandleAddPester())"
>@_addPesterButtonLabel</MudButton>
</MudItem>