简体   繁体   中英

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

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?

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.

You should not use a form element, nor should you submit the form. Luckily, as far as I know, internal Blazor code should stop the submission using preventDefault(); In any case, LoginAsync is probably called after the post back occurs. Just think about this: On the one hand your code initiates a "post back", on the other hand, it makes an http request to the server. In short, you should post your form data employing the HttpClient.

form data:

<div>
    <input type="email" placeholder="Email Address" bind="@LoginViewModel.Email" />
    <input type="password" placeholder="Password" bind="@LoginViewModel.Password" />
    <button type="button">Login</button>
</div>

Note that the type attribute of the button should be set to "button". Now, whenever you hit the button, the LoginAsync method would be called, from which you post your login data to the server.

Try the following:

Add these code snippets to your LoginService:

       [Inject]
        protected LocalStorage localStorage;
       // Note: LocalStorage is a library for storing data in Web Storage API. You may store your token in the LocalStorage, and retrieve it when you need to verify whether a user is authenticated. 

        // This method may be called from your NavMenu Component
        public bool IsAuthenticated()
        {
            var token = localStorage.GetItem<string>("token");

            return (token != null); 
        }




     public async Task<bool> LoginAsync(LoginViewModel model)
{
    try
    {
        var response = await _http.PostJsonAsync<TokenResult>("/api/auth", model);
        Token = response.Token;

         if (Token)
       {

          // Save the JWT token in the LocalStorage
          // https://github.com/BlazorExtensions/Storage
          await localStorage.SetItem<Object>("token", Token);


          // Returns true to indicate the user has been logged // in and the JWT token is saved on the user browser
         return true;

       }
    }
    catch (Exception)
    {
        return false;
    }
}

And finally NavMenu.cshtml:

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

// You also have to set the Startup class on the client as follows:

public void ConfigureServices(IServiceCollection services)
    {
        // Add Blazor.Extensions.Storage
       // Both SessionStorage and LocalStorage are registered
       // https://github.com/BlazorExtensions/Storage
       **services.AddStorage();**

      ...
    }

// Generally speaking this is what you've got to do on the client. // On the server, you've got to have a method, say in the Account controller, whose function is to generate the JWT token, you've to configure the JWT middleware, to annotate your controllers with the necessary attribute, as for instance:

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

Hope this helps...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM