I am using ASP.NET Core to build a simple login with multiple Blazor components.
I want to store my AppState such as IsLoggedIn inside my AppState code and subscribe to the OnChange method for UI updates.
namespace PulseVenture.PhotoGallery.Components.Services.State{ public class AppState { public Guid InstanceId { get; } = Guid.NewGuid(); public AppState() { Console.WriteLine($"AppState instance created at {DateTime.Now} with ID: {InstanceId}"); } public bool IsLoggedIn { get; private set; } public string? UserName { get; private set; } public event Action? OnChange; // LogIn Methode needs Validation @ToDo public void LogIn(string? userName) { IsLoggedIn = true; UserName = userName; NotifyStateChanged(); } public void LogOut() { IsLoggedIn = false; UserName = null; NotifyStateChanged(); } // Invoke, updating the UI or performing other actions //private void NotifyStateChanged() => OnChange?.Invoke(); private void NotifyStateChanged() { Console.WriteLine($"NotifyStateChanged invoked. OnChange is {(OnChange == null ? "null" : "not null")}. with Instance: {InstanceId}"); if (OnChange == null) { Console.WriteLine($"Warning: OnChange is null when NotifyStateChanged is called. with Instance: {InstanceId}"); } OnChange?.Invoke(); } }}Inside my components such as the NavMenu I am injecting the AppState.
@using Components.Services.State@implements IDisposable@inject AppState AppState...@if (AppState.IsLoggedIn){<Layout.NavComponents.UploadButton />}else{<Layout.NavComponents.LogInButton />}...@code { //SingOut Logic private void LogOut() => AppState.LogOut(); // subscribe to the OnChange event and trigger a re-render with StateHasChanged protected override void OnInitialized() { Console.WriteLine($"Component initialized with AppState instance: {AppState.InstanceId}"); if (AppState != null) { AppState.OnChange += () => { Console.WriteLine($"State changed detected in component. with Instance: {AppState.InstanceId}"); StateHasChanged(); }; Console.WriteLine($"Subscribed to AppState.OnChange with Instance: {AppState.InstanceId}"); } else { Console.WriteLine("AppState is null in OnInitialized"); } } public void Dispose() { if (AppState != null) { Console.WriteLine($"Disposed instance: {AppState.InstanceId}"); AppState.OnChange -= StateHasChanged; } }}And inside my LogInButton is a child component named LogInPopUp which also uses the AppState:
@rendermode InteractiveServer@using Components.Services.State@implements IDisposable@inject AppState AppState<div class="popup-backdrop" style="@(Show ? "display:block;" : "display:none;")"><div class="card popup-content"><button class="exit-btn" @onclick="ClosePopup"><svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z" /></svg></button><span class="card_title">Log In</span><p class="card_content">Upload Documents to share straight to your cliends.</p><div class="card_form"><input placeholder="Your Email" type="text" @bind="UserEmail"><button class="sign-in" @onclick="LogIn"> Sign In</button></div><p class="signup-link"> No Account?<a href="">Request Access</a></p></div></div>@code { [Parameter] public bool Show { get; set; } [Parameter] public EventCallback<bool> ShowChanged { get; set; } private async Task ClosePopup() { Show = false; if (ShowChanged.HasDelegate) { await ShowChanged.InvokeAsync(Show); } } // LogIn Logic private string UserEmail = string.Empty; private void LogIn() { AppState.LogIn(UserEmail); ClosePopup(); } protected override void OnInitialized() { Console.WriteLine($"Component initialized with AppState instance: {AppState.InstanceId}"); if (AppState != null) { AppState.OnChange += () => { Console.WriteLine($"State changed detected in component. with Instance: {AppState.InstanceId}"); StateHasChanged(); }; Console.WriteLine($"Subscribed to AppState.OnChange with Instance: {AppState.InstanceId}"); } else { Console.WriteLine("AppState is null in OnInitialized"); } } public void Dispose() { if (AppState != null) { Console.WriteLine($"Disposed instance: {AppState.InstanceId}"); AppState.OnChange -= StateHasChanged; } }}But when I run my code, I get the output that both elements subscribe to the appState and when they are done rendering they immediately dispose of the AppState and create a new instance.
Output:
AppState instance created at 21.11.2024 18:17:02 with ID: 872221af-23da-4427-8dc5-c5b5ac0e9f45Component initialized with AppState instance: 872221af-23da-4427-8dc5-c5b5ac0e9f45Subscribed to AppState.OnChange with Instance: 872221af-23da-4427-8dc5-c5b5ac0e9f45Component initialized with AppState instance: 872221af-23da-4427-8dc5-c5b5ac0e9f45Subscribed to AppState.OnChange with Instance: 872221af-23da-4427-8dc5-c5b5ac0e9f45Disposed instance: 872221af-23da-4427-8dc5-c5b5ac0e9f45Disposed instance: 872221af-23da-4427-8dc5-c5b5ac0e9f45I inject the AppState as a scoped DI.
I already tried using different DI injections but I technically need to use
builder.Services.AddScoped<AppState>();because my application can have more than one user.
Furthermore I tried passing the AppState down from the parent which also doesn't work.