Quantcast
Channel: Active questions tagged blazor - Stack Overflow
Viewing all articles
Browse latest Browse all 4839

In-line validation with dynamic components in Blazor/Mudblazor

$
0
0

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?


Viewing all articles
Browse latest Browse all 4839

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>