bindingblazorblazor-webassemblymudblazor

Blazor binding to property doesn't seem to fire from component


I'm working on a blazor webassembly application, using mudblazor's expansion panels. In a nutshell, I'm currently unable to correctly trigger them to expand, when they're bound to an object's property, which is changed from an internal component. I've oversimplified it below (or check out the trymudblazor (https://try.mudblazor.com/snippet/wuwyafQXojksUHSf) ).

I'm using the following model (ExpandModel):

public class ExpandModel
{
    private bool _expanded;
    public bool Expanded
    {
        get => _expanded;
        set
        {
            var isChanged = _expanded != value;
            _expanded = value;
            if (isChanged)
                ExpandedChanged.InvokeAsync();
        }
    }

    public EventCallback<bool> ExpandedChanged;
}

This is the component (ExpandComponent):

<MudButton OnClick="ToggleInner">Toggle inner</MudButton>

@code {
    [Parameter]
    public ExpandModel Model { get; set; }

    private void ToggleInner()
    {
        Model.Expanded = !Model.Expanded;
    }
}

And this is the home page:

@page "/"

<PageTitle>Home</PageTitle>

<MudButton @onclick="ToggleOuter">Toggle Outer</MudButton>
<MudExpansionPanels>
    <MudExpansionPanel @bind-IsExpanded="Model.Expanded">
        <TitleContent>
            <ExpandComponent Model="@Model"></ExpandComponent>
        </TitleContent>
    </MudExpansionPanel>
</MudExpansionPanels>

@code{
    private ExpandModel Model = new ExpandModel();

    private void ToggleOuter()
    {
        Model.Expanded = !Model.Expanded;
    }
}

When I use "ToggleIOuter" it works exactly how I expect it to. "ToggleInner" changes the objects value, but it's not changing the UI, eventhough ExpandedChanged is invoked. I am able to make it work, if I somehow call StateHasChanged() in the homepage. But this doesn't seem to be the intended way. I'm fairly new to blazor, so I'm sure I'm missing something basic. Any suggestion would be greatly appreciated and my sincerest apologies if this is stupidly obvious, but it's driving me insane. Thanks in advance!


Solution

  • You can't use EventCallback in the way you're trying to. It needs to be in the Component class code.

    Here's a simplified version of you code:

    ExpandComponent

    <MudButton OnClick="ToggleInner">Toggle inner</MudButton>
    
    @code {
        [Parameter] public ExpandModel Model { get; set; } = new();
        [Parameter] public EventCallback<ExpandModel> ModelChanged { get; set; }
    
        private void ToggleInner()
        {
            Model.Expanded = !Model.Expanded;
            this.ModelChanged.InvokeAsync(Model);
        }
    }
    
    public class ExpandModel
    {
        public bool Expanded { get; set; }
    }
    

    And Index

    @page "/"
    
    <PageTitle>Home</PageTitle>
    
    <MudButton @onclick="ToggleOuter">Toggle Outer</MudButton>
    <MudExpansionPanels>
        <MudExpansionPanel @bind-IsExpanded="Model.Expanded">
            <TitleContent>
                <ExpandComponent @bind-Model="@Model"></ExpandComponent>
            </TitleContent>
            <ChildContent>
                <h1>Expanded Content</h1>
            </ChildContent>
        </MudExpansionPanel>
    </MudExpansionPanels>
    
    @code {
        private ExpandModel Model = new ExpandModel();
    
        private void ToggleOuter()
        {
            Model.Expanded = !Model.Expanded;
        }
    }
    

    This works because the bind EventCallback is treated as a UI event by Index. It's handled by ComponenbtBase's IHandleEvent implementation which calls StateHasChanged for you.

    You are correct about StateHasChanged. There are only some very specific circumstances where you need to call it manually. In general, if you call it to make your component update, your logic is flawed.