0

I have a class / service in my Blazor MAUI application that regularly manipulates the data that is stored within it. There is an internal schedular on a fixed interval that regenerates the value.

I have a Blazor component that reads the value from this service. When the value changes in my service, I would like my Blazor component to reflect that change.

Just to keep it simple, lets take the following:

public class EmployeeService {
    
    public int NumberOfEmployees { get; private set; }

    public EmployeeService() {
        // Logic to initialize a fixed scheduled for function
        // RecalculateNumberOfEmployees();
    }

    private async void RecalculateNumberOfEmployees() {
        numberOfEmployees += 1;
    }
}
@path "/employees"
@inject EmployeeService Service

Number of employees: @Service.NumberOfEmployees

@code {
    
}

I found a recommendation here that uses a timer to invoke StateHasChanged() but I really, really don't like that approach. It seems like it is a waste of resources and an anti-pattern.

My next step is to make EmployeeService accept EventCallback from the Blazor component and store that in a list. This would allow any component to listen for changes in the EmployeeService class. When a component is unmounted, it will delete the callback.

Something like:

EmployeeService.cs

    public List<EventCallback> listeners { get; private set; } = new List<EventCallback>();

    public async void RegisterCallback(EventCallback callback) {
        listeners.ForEach(...notify listeners);
    }

    public async void DeregisterCallback(EventCallback callback) {
        listeners.Remove ... etc
    }

Employees.razor

...

@code {

    // register / deregister callback and listen for changes, invoke StateHasChanged()

}

Before I go down this route, are there any better design patterns that I could use for future components that would be better suited for this purpose? I feel like this is something that should already be baked into the framework but haven't seen anything that addresses it.

user0000001
  • 2,092
  • 2
  • 20
  • 48
  • See my answer to this question on how to notify Components from services - https://stackoverflow.com/questions/69558006/how-can-i-trigger-refresh-my-main-razor-page-from-all-of-its-sub-components-wit – MrC aka Shaun Curtis Aug 31 '22 at 21:25

1 Answers1

1

You could use an event Action:

EmployeeService.cs

public class EmployeeService
{
    public int NumberOfEmployees { get; private set; }

    public EmployeeService() {
        // Logic to initialize a fixed scheduled for function
        // RecalculateNumberOfEmployees();
    }

    private async void RecalculateNumberOfEmployees() {
        numberOfEmployees += 1;
        NotifyStateChanged();
    }
    
    public event Action OnChange;
    private void NotifyStateChanged() => OnChange?.Invoke();
}

Employees.razor

@inject EmployeeService EmployeeService
@implements IDisposable

Number of employees: @EmployeeService.NumberOfEmployees

@code {
    protected override void OnInitialized()
    {
        EmployeeService.OnChange += OnChangeHandler;
    }

    public void Dispose()
    {
        EmployeeService.OnChange -= OnChangeHandler;
    }

    private async void OnChangeHandler()
    {
        await InvokeAsync(StateHasChanged);
    }
}

Another possibility is to use the Event Aggregator pattern. Take a look at this library: https://github.com/mikoskinen/Blazor.EventAggregator

Dimitris Maragkos
  • 8,932
  • 2
  • 8
  • 26
  • Do you know if a circuit disconnection triggers Dispose? – Bennyboy1973 Aug 31 '22 at 21:50
  • I am getting `The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state` on the `NotifyStateChange()` definition. – user0000001 Aug 31 '22 at 21:56
  • 1
    You need to use InvokeAsync to execute StateHasChanged on the UI thread. Updated my answer, also check here: https://stackoverflow.com/questions/56477829/how-to-fix-the-current-thread-is-not-associated-with-the-renderers-synchroniza – Dimitris Maragkos Aug 31 '22 at 22:30
  • The .NET naming convention is for the `On*` method to raise events and not the actual events themselves nor the event handlers. So and `OnChange` would raise the event in the `EmployeeService` class. `Change` would be the name of the event and `EmployeeService_Change` would typically be the name of the handler. – Enigmativity Aug 31 '22 at 22:42
  • @Enigmativity you are probably right buy they didn't use this naming convention the documentation: https://learn.microsoft.com/en-us/aspnet/core/blazor/state-management?view=aspnetcore-6.0&pivots=webassembly#in-memory-state-container-service-wasm. Edited my answer according to your suggestions. – Dimitris Maragkos Aug 31 '22 at 23:03
  • 1
    @DimitrisMaragkos Thank you good sir! Works like a charm. Does this work if there are multiple "listeners"? – user0000001 Aug 31 '22 at 23:28
  • Yes, it works with multiple listeners. – Dimitris Maragkos Aug 31 '22 at 23:35
  • @DimitrisMaragkos - That's horrible. So much for clear standards. – Enigmativity Sep 01 '22 at 00:06
  • @Enigmativity: So much for clear standards. Do you mean the naming of events by Microsoft, as for instance, calling the `change` event `onchange`, the `click` event `onclick`, and so forth. Microsoft has been using event names with `on` for ages now. – enet Sep 01 '22 at 03:15
  • @enet - Yes, but against their own standards. – Enigmativity Sep 01 '22 at 03:36