Blazor - Bind to a property of a service, changed by another component

Viewed 2598

Update (Solution)

Thanks to Mister Magoo's answer I've got it working. The solution is done with events and is also shown in the official sample project FlightFinder.

Make sure you use a singleton:

Example: Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<LoginService, ILoginService>();
}

LoginService.cs:

public event Action OnChange;
public async Task<bool> LoginFromLocalStorageAsync()
{
    var response = await _http.PostJsonAsync<TokenResult>("/api/auth", model);
    Token = response.Token;
    ExpireDate = response.ExpireDate;
    _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
    OnChange?.Invoke(); // Here we invoke the event
}

NavMenu.cshtml:

protected override void OnInit()
{
    LoginService.OnChange += StateHasChanged;
}

Initial Question

I'm currently trying and learning blazor. I have a NavMenu Component which has multiple links, one of them being: <a href="login">Login</a>, which should change to <a onclick="@Logout">Logout</a> as soon as the user logs in. The login happens in another component (Login Component) using my own service LoginService.

LoginService has a Token property for the Bearer Token and a property public bool IsLoggedIn => !string.IsNullOrEmpty(Token);. I tried to use a simple binding with an if-else-statement in the razor view. That didn't work, my next try was using StateHasChanged(); in my Login component, as soon as someone logs in. Didn't work either (probably because I want to update NavMenu and not Login...)

NavMenu.cshtml:

@inject ILoginService LoginService 
@if(LoginService.IsLoggedIn) {
    <a href="logout">Logout</a>
}
else {
    <a href="login">Login</a>
}

Login.cshtml:

<form onsubmit="@Submit">
    <input type="email" placeholder="Email Address" bind="@LoginViewModel.Email" />
    <input type="password" placeholder="Password" bind="@LoginViewModel.Password" />
    <button type="submit">Login</button>
</form>

@functions
{
    public LoginViewModel LoginViewModel { get; } = new LoginViewModel();
    public async Task Submit()
    {
        await LoginService.LoginAsync(LoginViewModel);
    }
}

LoginService.cs

public class LoginService : ILoginService
{
    private readonly HttpClient _http;
    public LoginService(HttpClient http) => _http = http;
    public string Token { get; private set; }
    public bool IsLoggedIn => !string.IsNullOrEmpty(Token);
    public async Task<bool> LoginAsync(LoginViewModel model)
    {
        try
        {
            var response = await _http.PostJsonAsync<TokenResult>("/api/auth", model);
            Token = response.Token;
            return true;
        }
        catch (Exception)
        {
            return false;
        }
    }
}

Unfortunately, NavMenu stays on <a href="login">Login</a>. I was thinking about sending a message to NavMenu from the Login Component. How do I get NavMenu to update its view?

2 Answers

You can add an event to the LoginService, which you raise whenever your Token changes.

Then your menu component can subscribe to that event (you already have the LoginService injected) and call StateHasChanged().

This will refresh the view and update the client.

Your requirement to place the Spinner component in the MainLayout to make them available to all pages can be achieved easily by using the Visible API which is a two-way binding property. We have also prepared a sample for your reference,

Code snippet: MainLayout.razor:

@using Syncfusion.Blazor.Spinner 
<CascadingValue Value="@this"> 
    <div class="page"> 
        <div class="sidebar"> 
            <NavMenu /> 
        </div> 
 
        <div class="main"> 
            <div class="top-row px-4"> 
                <a href=http://blazor.net target="_blank" class="ml-md-auto">About</a> 
            </div> 
 
            <div class="content px-4"> 
                @Body 
            </div> 
 
        </div> 
        <SfSpinner @bind-Visible="@SpinnerVisible" CssClass="e-spin-overlay"> 
        </SfSpinner> 
    </div> 
</CascadingValue> 
@code{ 
    public bool SpinnerVisible { get; set; } = false; 
 
    public async Task ClickHandler() 
    { 
        this.SpinnerVisible = true; 
        StateHasChanged(); 
        await Task.Delay(2000); 
        this.SpinnerVisible = false; 
        StateHasChanged(); 
    } 
} 
 

Index.razor

<div> 
    <SfButton @onclick="@ClickHandler">Show Spinner</SfButton> 
</div> 
 
@code{ 
    [CascadingParameter] 
    public MainLayout mainLayoutObj { get; set; } 
    private async Task ClickHandler() 
    { 
        await mainLayoutObj.ClickHandler(); 
    } 
} 

This ClickHandler method can be called anywhere from the page depends on the usage,

Note: Use CascadingValue in the MainLayout to access the main layout anywhere in the body.

Sample: https://www.syncfusion.com/downloads/support/directtrac/342190/ze/Web_spinner848284331

Please check the above code snippet and sample and let us know if it satisfies your requirement.

Regards, Vinitha.

Related