I am new to Blazor, but I have a strong background in React. I suspect I might be trying to make Blazor work like React, and that could be causing my issue.
I have a BaseStep component that provides a structure for child steps in a multi-step form. One such step is StepOne, which contains a ToggleGroup component that allows users to select an engagement type.
However, the UI does not update/re-render when @AccessRequest.NatureOfEngagementId changes. Strangely enough:
The <p>
tag correctly prints the updated ID, meaning the data itself is changing.
The selection in ToggleGroup only updates when another part of the page causes a re-render.
This leaves me thinking the issue lies in my ToggleGroup component. I need help figuring out why my UI is not updating immediately when @AccessRequest.NatureOfEngagementId changes.
I have a BaseStep
component that acts as an abstract step:
public abstract class StepBase : ComponentBase
{
[Parameter] public Request NewAccessRequest { get; set; } = default!;
[Parameter] public EventCallback<Request> NewAccessRequestChanged { get; set; } = default!;
[Parameter] public EventCallback<bool> OnStepValidChanged { get; set; } = default!;
public required MudForm StepForm;
protected bool StepSuccess;
// Local copy of the request for binding
private Request _localRequest = default!;
// Property that binds to the local request and updates parent when changed
protected Request AccessRequest
{
get => NewAccessRequest;
set
{
if (!EqualityComparer<Request>.Default.Equals(NewAccessRequest, value))
{
NewAccessRequestChanged.InvokeAsync(value);
}
}
}
// Ensure local copy is in sync when parent updates the parameter
protected override void OnParametersSet()
{
if (!EqualityComparer<Request>.Default.Equals(NewAccessRequest, _localRequest))
{
_localRequest = NewAccessRequest;
}
}
// Method to validate the step
public async Task<bool> ValidateStep()
{
if (StepForm != null)
{
await StepForm.Validate();
await OnStepValidChanged.InvokeAsync(StepSuccess);
}
return StepSuccess;
}
}
This child component inherits from StepBase
and updates AccessRequest
when an engagement is changed.
public partial class StepOne : StepBase
{
[Parameter] public List<NatureOfEngagement> Engagements { get; set; } = [];
private void OnEngagementChanged(int newEngagementId)
{
AccessRequest = RequestBuilder.From(AccessRequest)
.WithNatureOfEngagementId(newEngagementId)
.Build();
}
}
Child Component's UI:
@inherits StepBase
<MudStep Title="What relationship does the person requesting access have with the company">
<MudForm Model="@NewAccessRequest" @ref="StepForm" @bind-IsValid="StepSuccess">
<MudGrid>
<MudItem xs="12">
<ToggleGroup T="int"
Required
RequiredError="Please select an option"
Value="@AccessRequest.NatureOfEngagementId"
ValueChanged="OnEngagementChanged"
SelectionMode="SelectionMode.SingleSelection"
Options="Engagements.Select(e =>
new Option<int> { Value = e.Id, Label = e.Engagement
}).ToList()" />
</MudItem>
<p>Selected ID @AccessRequest.NatureOfEngagementId</p>
</MudGrid>
</MudForm>
</MudStep>
This custom ToggleGroup<T>
component is used for selection:
public partial class ToggleGroup<T> : MudFormComponent<T, T>
{
// The text to display above the toggle group
[Parameter] public string? Label { get; set; }
[Parameter] public Dictionary<string, object>? AdditionalAttributes { get; set; }
// The list of toggle options
[Parameter] public IEnumerable<Option<T>>? Options { get; set; }
[Parameter] public SelectionMode SelectionMode { get; set; } = SelectionMode.SingleSelection;
[Parameter]
[Category(CategoryTypes.List.Behavior)]
public EventCallback<T?> ValueChanged { get; set; }
[Parameter]
[Category(CategoryTypes.List.Behavior)]
public EventCallback<IEnumerable<T?>?> ValuesChanged { get; set; }
[Parameter]
public T? Value { get; set; }
[Parameter]
public IEnumerable<T>? Values { get; set; }
// Single selection binding
private T? SingleSelectedValue
{
get => Value;
set
{
if (!EqualityComparer<T?>.Default.Equals(_value, value))
{
_value = value;
Value = value;
Touched = true;
BeginValidateAsync();
ValueChanged.InvokeAsync(value);
}
}
}
// Multiple selection binding
private IEnumerable<T>? MultiSelectedValues
{
get => Values;
set
{
if (!EqualityComparer<IEnumerable<T>?>.Default.Equals(_value as IEnumerable<T>, value))
{
_value = value != null ? (T?)(object?)value : default;
Touched = true;
Values = value;
ValuesChanged.InvokeAsync(value);
BeginValidateAsync();
}
}
}
public ToggleGroup() : base(converter: new MudBlazor.Converter<T, T>())
{
}
protected override Task ValidateValue()
{
var errors = new List<string>();
ValidationErrors.Clear();
if (Options == null || !Options.Any())
{
// If there are no available options, mark as error
Error = Required;
}
else
{
if (SelectionMode == SelectionMode.SingleSelection)
{
// Ensure SingleSelectedValue is present in Options
Error = Required && (SingleSelectedValue == null || !Options.Any(o => EqualityComparer<T>.Default.Equals(o.Value, SingleSelectedValue)));
}
else
{
// Ensure all MultiSelectedValues exist in Options
Error = Required && (MultiSelectedValues == null || !MultiSelectedValues.All(val => Options.Any(o => EqualityComparer<T>.Default.Equals(o.Value, val))));
}
}
if (Error)
{
ValidationErrors.Add(RequiredError);
}
ValidationErrors = errors;
return Task.CompletedTask;
}
}
Toggle Component UI:
@typeparam T
@inherits MudFormComponent<T, T>
<MudStack>
@if (Label != null)
{
<MudText Class="m-2" Typo="Typo.body2" Color="@(Error ? Color.Error : Color.Default)">
@(Label + (Required ? "*" : ""))
</MudText>
}
@if (SelectionMode == SelectionMode.SingleSelection)
{
<!-- Single Selection -->
<MudToggleGroup T="T"
CheckMark
SelectionMode="SelectionMode.SingleSelection"
Color="@(Error ? Color.Error: Color.Primary)"
@bind-Value="SingleSelectedValue"
@attributes="@AdditionalAttributes">
@foreach (var option in Options ?? Enumerable.Empty<Option<T>>())
{
<MudToggleItem Value="@option.Value"
UnselectedIcon="@Icons.Material.Filled.CheckBoxOutlineBlank"
SelectedIcon="@Icons.Material.Filled.CheckBox">
@option.Label
</MudToggleItem>
}
</MudToggleGroup>
}
else
{
<!-- Multiple Selection -->
<MudToggleGroup T="T"
CheckMark
SelectionMode="SelectionMode.MultiSelection"
Color="@(Error ? Color.Error: Color.Primary)"
@bind-Values="MultiSelectedValues"
@attributes="@AdditionalAttributes">
@foreach (var option in Options ?? Enumerable.Empty<Option<T>>())
{
<MudToggleItem Value="@option.Value"
UnselectedIcon="@Icons.Material.Filled.CheckBoxOutlineBlank"
SelectedIcon="@Icons.Material.Filled.CheckBox">
@option.Label
</MudToggleItem>
}
</MudToggleGroup>
}
@if (Error)
{
<MudText Class="text-danger" Style="margin-top: -10px; margin-left: 8px;" Typo="Typo.caption">
@(string.IsNullOrWhiteSpace(ErrorText) ? RequiredError : ErrorText)
</MudText>
}
</MudStack>
The issue is that the child component's UI does not re-render when the @AccessRequest.NatureOfEngagementId
property changes. The correct ID is printed inside the <p>
tag, but the ToggleGroup does not visually update unless another re-render is triggered elsewhere.
I have been on this for sometime now and would love if another eye was to look at this maybe they can spot the issue.
It turned out my code was just fine. It turned out the MudBlazor component is still in development.