Search code examples
asp.net-coreblazorblazor-component

Blazor binding to read-only attribute of a child


Here is the example code:

Parent.razor:

@page "/"

<ChildComponent @ref="Child"/>
<br/>
Parent Total: @(Child?.Total ?? 0)
@code {
    public ChildComponent Child { get; set; }
}

ChildComponent.razor:

@for (int i = 0; i < 10; i++)
{
    int i1 = i;
    <input type="checkbox" @onchange="v => OnSelect((bool)v.Value, i1)"/>
}
<br/>
Child Total: @Total
@code {
    public List<int> SelectedItems { get; set; } = new List<int>();
    public int Total => SelectedItems.Sum();

    public void OnSelect(bool selected, int index)
    {
        if (selected)
        {
            SelectedItems.Add(index);
        }
        else
        {
            SelectedItems.Remove(index);
        }
    }
}

(Blazor Fiddle: https://blazorfiddle.com/s/srmk6ehc)

Note that the @Total in the child component gets updated automatically in response to the checkboxes being clicked or unclicked, but the @Child.Total in the parent component never gets updated. How do I bind in such a way that the parent value will be redrawn when something occurs in the child component?

Note: This is just example code demonstrating the issue, please focus on the question of propagating updates from a child's readonly property, not other ways I could completely restructure the code.


Solution

  • Your example is somewhat contrived, but first a couple of points:

    1. Wiring up component instances using @ref is not a sustainable way to develop the Blazor UI. There are places where you need to do it, but in general it's not good practice.
    2. There is no build in mechanism for a parent component to detect state change and render events in it's child components.

    There are two commonly used mechanisms to communicate between components:

    1. On a parent => child relationship with simple information you can using binding. This is appropriate for edit type controls.
    2. Separate out state from your component and maintain that state in either a DI Scoped Service object, a cascaded instance from the parent, or use something like a Flux implementation.

    Here, I've used the Bind method [in a slightly contrived way].

    What you don't see in the bind process is the code generated by the Razor compiler in the parent. The callback is handled as a UI event in the parent which triggers the ComponentBase render process.

    The component:

    @for (int i = 0; i < 10; i++)
    {
        int i1 = i;
        <input class="form-check-inline" type="checkbox" @onchange="v => OnSelect(v, i1)" />
    }
    
    <br />
    Child Total: @Total
    
    @code {
        // Two Bind Parameters - we don't need the ValueExpression
        [Parameter] public int Total { get; set; }
        [Parameter] public EventCallback<int> TotalChanged { get; set; }
    
        private List<int> SelectedItems { get; set; } = new List<int>();
    
        public async Task OnSelect(ChangeEventArgs e, int index)
        {
            // Conversion here to keep the markup clean
            var selected = (bool?)e.Value ?? false;
    
            if (selected)
                SelectedItems.Add(index);
    
            else
                SelectedItems.Remove(index);
    
            // Call the eventcallback 
            await this.TotalChanged.InvokeAsync(this.SelectedItems.Count());
        }
    }
    

    Demo Page:

    @page "/"
    
    <ChildComponent @bind-Total="_total"  />
    
    <div class="alert alert-primary">@_total</div>
    
    @code {
        private int _total;
    }
    

    The other answer shows how to use state objects. You can find other examples if you search on SO for "Blazor Notification Pattern".