Search code examples
c#blazor.net-8.0

How to iterate over unknown number of components on a page in Blazor


My Blazor (.NET 8) page has a list of components generated one for each row in a database query response:

@foreach (var item in _listOfItems)
{
    <CollapsibleCard Title="item.CardTitle">
        other contest based on item..
    </CollapsibleCard>
}

where CollapsibleCard is a component I made that wraps a Bootstrap card, adding a collapse/expand button, and the collapsed/expanded state is managed using Blazor code (i.e. not at the Javascript level).

I would like to have a button on the page that collapses or expands all the cards at once. The component has a method to collapse or expand it. What code can I put in the onclick handler for the button to go through all these cards?

For a fixed number of cards I could use @ref definition on each one, but not sure how that would work in this scenario where there could be any number of components.

NB. I have previously tried having a Parameter for the expanded/collapsed state, but generally found that approach not to work: the compiler gives warnings if you try to modify the value of a Parameter from inside the component (as would happen when the user clicks on the button in the card to expand it), and changing the value of the parameter from outside the component doesn't do anything as the component doesn't know to re-render itself, you would have to iterate over the components to re-render them anyway, or re-render the entire page.


Solution

  • You need to use a cascading state object with an event. Using @ref is not the way to go. This is a common notification pattern used in Blazor. If you search SO for "Blazor Notification Pattern" you will find several answers on the subject.

    You can either cascade the state object for a Form level context or register it as a scoped service for an SPA Session level context.

    Here's a quick and dirty demo.

    public class CardState
    {
        public bool AllOpen { get; private set; }
        public event EventHandler<bool>? StateChanged;
    
        public void CloseAllCards()
        {
            AllOpen = false;
            this.StateChanged?.Invoke(this, this.AllOpen);
        }
    
        public void OpenAllCards()
        {
            AllOpen = true;
            this.StateChanged?.Invoke(this, this.AllOpen);
        }
    }
    

    A very simple card

    @implements IDisposable
    
    @if (_isOpen)
    {
        <div class="card my-2">
            <div class="m-2 text-end">
                <button class="btn btn-dark" @onclick="this.Close">Close</button>
            </div>
            <div class="card-body">Basic card</div>
        </div>
    }
    @code {
        [CascadingParameter] private CardState? CardState { get; set; }
        [Parameter] public bool IsInitiallyOpen { get; set; } = true;
        private bool _isOpen = true;
    
        protected override void OnInitialized()
        {
            _isOpen = this.IsInitiallyOpen;
            if (this.CardState is not null)
                this.CardState.StateChanged += this.OnStateChanged;
        }
    
        protected void Close()
        {
            _isOpen = false;
        }
    
        private void OnStateChanged(object? sender, bool state)
        {
            _isOpen = state;
            // must call here as this is not a UI event
            this.StateHasChanged();
        }
    
        public void Dispose()
        {
            if (this.CardState is not null)
                this.CardState.StateChanged -= this.OnStateChanged;
        }
    
    }
    

    And Demo page

    @page "/"
    
    <PageTitle>Home</PageTitle>
    
    <h1>Hello, world!</h1>
    
    <div class="m-2 p-2 text-end">
        <button class="btn btn-primary" @onclick="ChangeState">Change</button>
    </div>
    
    <CascadingValue Value="_cardState">
        <Card />
        <Card />
        <Card />
        <Card />
    </CascadingValue>
    
    @code {
        private readonly CardState _cardState = new();
    
        private void ChangeState()
        {
            if (_cardState.AllOpen)
                _cardState.CloseAllCards();
    
            else
                _cardState.OpenAllCards();
        }
    }