eventsblazorrerender

Best Way to Trigger Re-Rendering in Blazor Following Data Changes


I have a Blazor Server app with a main layout component that contains several child components. Each component registers for an event that's defined in a static class. When the user clicks a button in Child1 (for example), it changes some data in a service class and then invokes the static class to Throw the event that then triggers Child2 to re-render itself (using InvokeAsync(StateHasChanged) in its event handler), causing Child2 to reflect the new data. A different approach is to have each component register for an event that's defined and triggered by the data service as further described in this example.

Is here any reason to favor the data service event over the static class one? Either seems to work, though perhaps the service-based one is a bit more elegant/tight. On the other hand, the static class version can be used for changes to more than one data service.


Solution

  • Consider the scope of your state tracking object and your component/page. Ideally, they should match.

    Your static class doesn't. It's shared by all users and all instances of the page.

    My original answer, which you quoted here, doesn't either. It's something I've been working on for a while. DotNetCore doesn't provide us with a DI container we can scope to the component/page. OwningComponentBase recognises the problem and attempts to solve it, but isn't fit for purpose.

    Consider the following Counter page implementation.

    A Counter State object:

    public class CounterState : IDisposable
    {
        // This is here simply to demonstrate Dependancy Injection
        private NavigationManager _navigationManager;
        
        public int Counter { get; private set; }
    
        public event EventHandler<CounterChangedEventArgs>? CounterChanged;
    
        public CounterState(NavigationManager navigationManager)
            => _navigationManager = navigationManager;
    
        public void IncrementCounter(object? sender)
        {
            this.Counter++;
            this.CounterChanged?.Invoke(sender, CounterChangedEventArgs.Create(this.Counter));
        }
    
        // implementated to demonstrate dealing with an IDisposable State object
        public void Dispose() { }
    }
    

    And a custom EventArgs

    public class CounterChangedEventArgs : EventArgs
    {
        public int CounterValue { get; set; }
    
        public static CounterChangedEventArgs Create(int value)
            => new CounterChangedEventArgs {  CounterValue = value };
    }
    

    A simple Counter display component:

    @implements IDisposable
    
    <div class="bg-dark text-white m-2 p-2">
        <pre>Value : @_counterState?.Counter </pre>
    </div>
    
    @code {
        [CascadingParameter] private CounterState _counterState { get; set; } = default!;
    
        protected override void OnInitialized()
        {
            ArgumentNullException.ThrowIfNull(_counterState);
            _counterState.CounterChanged += this.OnCounterChanged;
        }
    
        private void OnCounterChanged(object? sender, CounterChangedEventArgs e)
            => this.InvokeAsync(StateHasChanged);
    
        public void Dispose()
            => _counterState.CounterChanged -= this.OnCounterChanged;
    }
    

    And an Incrementer component:

    <div class="m-2 p-2">
        <button class="btn btn-primary" @onclick=this.OnIncrementCounter>Increment</button>
    </div>
    
    @code {
        [CascadingParameter] private CounterState? _counterState { get; set; }
    
        protected override void OnInitialized()
        =>  ArgumentNullException.ThrowIfNull(_counterState);
    
        private Task OnIncrementCounter()
        {
            _counterState?.IncrementCounter(this);
            return Task.CompletedTask;
        }
    }
    

    And finally the revised Counter page:

    @page "/counter"
    @implements IDisposable
    @inject IServiceProvider serviceProvider
    
    <PageTitle>Counter</PageTitle>
    
    <h1>Counter Page</h1>
    <CascadingValue Value="_counterState">
        <CounterViewer />
        <CounterViewer />
        <CounterViewer />
        <CounterViewer />
        <CounterViewer />
        <div class="row">
            <div class="col-2">
                <CounterButton />
            </div>
            <div class="col-2">
                <CounterButton />
            </div>
            <div class="col-2">
                <CounterButton />
            </div>
            <div class="col-2">
                <CounterButton />
            </div>
        </div>
    </CascadingValue>
    
    @code {
        private CounterState? _counterState;
    
        protected override void OnInitialized()
        {
            // demonstrates creating an object instance in the context of thw Service Container
            // this will inject any defined dependencies  (in our case the Scoped NavigationManager)
            _counterState = ActivatorUtilities.CreateInstance<CounterState>(serviceProvider);
            ArgumentNullException.ThrowIfNull(_counterState);
        }
    
        // This will get called by the Renderer when the page/component goes out of scope
        public void Dispose()
            => _counterState?.Dispose();
    }
    

    This implementation:

    1. Creates a object to track state and raise events to notify state changes.
    2. The object has the same scope as the page.
    3. The page cascades the state object.
    4. Sub components capture the cascaded state and call methods to update the state and/or register event handlers to react to state changes.
    5. The page creates the state object in the DI container context, so any required DI services get injected.

    The important feature of this pattern is matching the scope of the state object to it's owning component. Using DI: Scoped is too broad and Transient too narrow [each component gets a new instance]. If you cascade a Transient obtained service that implements IDisposable, you create a memory leak. Creating the instance using ActivatorUtilities solves the DI issues. However you are responsible for implementing disposal.

    There's an article here that covers the subject in more detail - https://www.codeproject.com/Articles/5352916/Matching-Services-with-the-Blazor-Component-Scope.