Search code examples
blazorblazor-server-sidemudblazor

MudRating.SelectedValueChanged() called twice on a click, second time with the original Value


I have the following three tier setup.

The main razor file has:

<Rating @bind-Value="@Model.RoiRating" Descriptions="@Rating.RoiDescriptions"/>

Rating.razor is:

<MudRating SelectedValue="Value" SelectedValueChanged="OnSelectedValueChanged" HoveredValueChanged="OnHoveredValueChanged" />
<MudText Typo="Typo.subtitle2" Class="deep-purple-text mt-2">@RatingText</MudText>

And Rating.razor.cs is:

public partial class Rating
{
    public static string[] RoiDescriptions = new[]
    {
        "Rate the ROI",
        "Damaging",
        "Negative",
        "Ineffective",
        "Helpful",
        "Awesome!"
    };

    [Parameter]
    public string[] Descriptions { get; set; } = RoiDescriptions;

    [Parameter]
    public int Value { get; set; }

    [Parameter]
    public EventCallback<int> ValueChanged { get; set; }

    private int? _activeValue;

    private void OnHoveredValueChanged(int? val) => _activeValue = val;

    private string RatingText
    {
        get
        {
            if (_activeValue is null or < 1 or > 5)
                return Descriptions[0];
            return Descriptions[_activeValue.Value];
        }
    }
}

The third (innermost) tier is the MudRating control.

When I click on star #3, in the debugger I see the following calls:

OnSelectedValueChanged(3)
OnSelectedValueChanged(0)

Where/why is that second call occurring? Is it re-reading Value and setting back to that? I can't change Value per this guidance from Microsoft.

So what's going on and how do I address this? Note: This question is an offshoot of How do I cascade a @bind-Value?


Solution

  • MisterMagoo is correct in his assertion on the re-render.

    This solution also works. It turns off the built in rendering caused by ComponentBase. All the extraneous rendering goes away and the component re-renders when the parent renders and Value has changed.

    @implements IHandleEvent
    
    <div class="d-flex flex-column align-center">
        <MudRating @bind-SelectedValue:get="this.Value" @bind-SelectedValue:set="this.OnSelectedValueChanged" HoveredValueChanged="HandleHoveredValueChanged" />
        <MudText Typo="Typo.subtitle2" Class="deep-purple-text mt-2">@LabelText</MudText>
    </div>
    
    @code {
        [Parameter] public int Value { get; set; }
    
        [Parameter] public EventCallback<int> ValueChanged { get; set; }
    
        private int? activeVal;
    
        private void HandleHoveredValueChanged(int? val) => activeVal = val;
    
        private async Task OnSelectedValueChanged(int rating)
        {
            await ValueChanged.InvokeAsync(rating);
        }
    
        private string LabelText => (activeVal ?? this.Value) switch
        {
            1 => "Very bad",
            2 => "Bad",
            3 => "Sufficient",
            4 => "Good",
            5 => "Awesome!",
            _ => "Rate our product!"
        };
    
        //  override the ComponentBase handler so 
        //  no automated StateHasChanged calls are made
        Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
        {
            return callback.InvokeAsync(arg);
        }
    }
    

    To demonstrate this is not normal behaviour here's a simpler Non-MudBlazor version of the control using a range input with some debug code to output the events to Output:

    @using System.Diagnostics;
    
    <div class="d-flex flex-column align-center">
        <label for="customRange2" class="form-label">Rate Us</label>
        <input @bind:event="oninput" @bind:get="this.Value" @bind:set="this.OnSelectedValueChanged" type="range" class="form-range" min="1" max="5">
        <div>@LabelText</div>
        @{
            Debug.WriteLine($"Rating - Rendered Value={this.Value}");
        }
    </div>
    
    @code {
        [Parameter] public int Value { get; set; }
    
        [Parameter] public EventCallback<int> ValueChanged { get; set; }
    
        public override Task SetParametersAsync(ParameterView parameters)
        {
            Debug.WriteLine("Rating - SetParameters Called");
            return base.SetParametersAsync(parameters);
        }
    
        private async Task OnSelectedValueChanged(int rating)
        {
            Debug.WriteLine($"Rating - OnSelectedValueChanged Called rating = {rating}");
            await ValueChanged.InvokeAsync(rating);
            Debug.WriteLine($"Rating - OnSelectedValueChanged Completed rating = {rating}");
        }
    
    
        private string LabelText => (this.Value) switch
        {
            1 => "Very bad",
            2 => "Bad",
            3 => "Sufficient",
            4 => "Good",
            5 => "Awesome!",
            _ => "Rate our product!"
        };
    }