Search code examples
c#blazorblazor-webassembly

Two-Way Component Parameter Binding on a class in Blazor?


I have a class MealsQueryInputs that I would like to use as a component parameter with two-way binding capabilities.

All of the demos and sample code I can find are using built-in primitive types and never a class. I can get the MS demos to work but I cannot get binding to a class to work. Is it even possible to do this?

My component FilterSortOptions.razor:

using WhatIsForDinner.Shared.Models

<MudCheckBox Checked="@QueryInputs.Favorite" 
             Color="Color.Inherit" 
             CheckedIcon="@Icons.Material.Filled.Favorite" 
             UncheckedIcon="@Icons.Material.Filled.FavoriteBorder"
             T="bool"/>
<MudRating SelectedValue="@QueryInputs.Rating"/>
<MudButton OnClick="@(async () => await OnPropertyChanged())">Apply</MudButton>

@code {
    [Parameter]
    public MealsQueryInputs QueryInputs { get; set; }

    [Parameter]
    public EventCallback<MealsQueryInputs> QueryInputsChanged { get; set; }

    private async Task OnPropertyChanged()
    {
        await QueryInputsChanged.InvokeAsync(QueryInputs);
    }
}

Solution

  • Updated Answer

    Firstly, if your using an object then you are passing around references to the same object. So when you update the object in the sub-component, you're updating the same object the parent is using. You don't need to pass the object back in the callback unless you create a noew copy of it.

    Secondly, your not binding the mud controls to the object.

    Let's look at your code:

    <MudCheckBox Checked="@QueryInputs.Favorite" 
                 Color="Color.Inherit" 
                 CheckedIcon="@Icons.Material.Filled.Favorite" 
                 UncheckedIcon="@Icons.Material.Filled.FavoriteBorder"
                 T="bool"/>
    

    Checked="@QueryInputs.Favorite" doesn't bind the control to the field. It just sets the initial value.

    I think (I don't use Mudblazor and it's a little different from standard Blazor Form Controls) you need to do this:

    <MudCheckBox @bind-Checked="@QueryInputs.Favorite"></MudCheckBox>
    

    The same is true for MudRating.

        <MudRating @bind-SelectedValue="@QueryInputs.Rating" />
    

    Then the button:

    <MudButton OnClick="@(async () => await OnPropertyChanged())">Apply</MudButton>
    

    can be simplified to this. You're wrapping an async method within an async method.

    <MudButton OnClick="OnPropertyChanged">Apply</MudButton>
    // or
    <MudButton OnClick="() => OnPropertyChanged()">Apply</MudButton>
    
    

    Original Answer

    There are a couple of issues here:

    1. QueryInputs is a Parameter and therefore should never be modified by the code within the component. You end up with a mismatch between what the Renderer thinks the value is and what it actually is.

    2. When the parent component renders it will always cause a re-render of any component that is passed a class as a parameter. The Renderer has no way of telling if a class has been modified, so it applies the heavy handed solution - call SetParametersAsync on the component.

    A solution is to use a view service to hold the data and events to notify changes. One version of the truth! Search "Blazor Notification Pattern" for examples of how to implement this. I'll post some code if you can't find what you want.