2

I am struggling to get JWT from SecureStorage, as it's Get method is async, and we all know that constructor doesn't support async calls.

UPDATE:

What I want to do is check if I have a token and at the app start show the LoginPage or the MainPage.

I tried something like this:

public AppShell()
    {
        JWTokenModel jwt = null;
        Task.Run(async () =>
        {
            jwt = await StorageService.Secure.GetAsync<JWTokenModel>(StorageKeys.Secure.JWT);
        });

        InitializeComponent();
        RegisterRoutes();

        shellContent.Title = "Amazons of Volleyball";

        if (jwt is null || jwt?.Expiration < DateTime.Now)
        {
            shellContent.Route = PageRoutes.LoginPage;
            shellContent.ContentTemplate = new DataTemplate(typeof(LoginPage));
        }
        else
        {
            shellContent.Route = PageRoutes.HomePage;
            shellContent.ContentTemplate = new DataTemplate(typeof(MainPage));
        }
    }

    private void RegisterRoutes()
    {
        Routing.RegisterRoute(PageRoutes.LoginPage, typeof(LoginPage));
        Routing.RegisterRoute(PageRoutes.HomePage, typeof(MainPage));
        Routing.RegisterRoute(PageRoutes.DetailsPage, typeof(PlayerDetailsPage));
        Routing.RegisterRoute(PageRoutes.AddOrUpdatePage, typeof(AddOrUpdatePlayer));
    }

When it hits the StorageService.Secure.GetAsync method's line, where I wan't to get the data like

public static async Task<T> GetAsync<T>(string key)
{
    try
    {
        var value = await SecureStorage.Default.GetAsync(key);

        if (string.IsNullOrWhiteSpace(value))
            return (T)default;

        var data = JsonSerializer.Deserialize<T>(value);
        return data;
    }
    catch(Exception ex)
    {
        return (T)default;
    }
}

it simple jumps out of the method.

UPDATE: I update the code suggested by ewerspej. The error still stands, when the SecureStore tries to get the value it jumps out from the method, no exception and I got the following error:

System.InvalidOperationException: 'No Content found for ShellContent, Title:, Route D_FAULT_ShellContent2'

The updated code:

public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();
        RegisterRoutes();

        SetShellContentTemplate();
    }

    private async void SetShellContentTemplate()
    {
        var hasValidJWT = await LoadTokenAsync();

        if (hasValidJWT)
        {
            shellContent.ContentTemplate = new DataTemplate(typeof(MainPage));
            shellContent.Route = PageRoutes.HomePage;
            shellContent.Title = "Amazons of Volleyball";
        }
        else
        {
            shellContent.ContentTemplate = new DataTemplate(typeof(LoginPage));
            shellContent.Route = PageRoutes.LoginPage;
            shellContent.Title = "Amazons of Volleyball";
        }
    }

    private async Task<bool> LoadTokenAsync()
    {
        var jwt = await StorageService.Secure.GetAsync<JWTokenModel>(StorageKeys.Secure.JWT);

        return !(jwt is null || jwt?.Expiration < DateTime.Now);
    }

    private void RegisterRoutes()
    {
        Routing.RegisterRoute(PageRoutes.LoginPage, typeof(LoginPage));
        Routing.RegisterRoute(PageRoutes.HomePage, typeof(MainPage));
        Routing.RegisterRoute(PageRoutes.DetailsPage, typeof(PlayerDetailsPage));
        Routing.RegisterRoute(PageRoutes.AddOrUpdatePage, typeof(AddOrUpdatePlayer));
    }
}

UPDATE 2: Moved the logic to App class:

public static class PageRoutes
{
    public static string LoginPage = "login";
    public static string HomePage = "home";
    public static string AddOrUpdatePage = "add-or-update";
    public static string DetailsPage = "/details";
}

public partial class App : Application
{
    private readonly ISecurityClient securityClient;

    public App(ISecurityClient securityClient)
    {
        this.securityClient = securityClient;

        InitializeComponent();
        SetStartPage();
    }

    private async void SetStartPage()
    {
        var hasValidJWT = await ReatJwtAsync();

        MainPage = hasValidJWT ?
                              new AppShell() :
                              new LoginPage(securityClient);
    }

    private async Task<bool> ReatJwtAsync()
    {
        var jwt = await StorageService.Secure.GetAsync<JWTokenModel>(StorageKeys.Secure.JWT);

        return !(jwt is null || jwt?.Expiration < DateTime.Now);
    }
}

public partial class AppShell : Shell
{
    public AppShell()
    {
        RegisterRoutes();
        InitializeComponent();
    }

    private void RegisterRoutes()
    {
        Routing.RegisterRoute(PageRoutes.LoginPage, typeof(LoginPage));
        Routing.RegisterRoute(PageRoutes.HomePage, typeof(MainPage));
        Routing.RegisterRoute(PageRoutes.DetailsPage, typeof(PlayerDetailsPage));
        Routing.RegisterRoute(PageRoutes.AddOrUpdatePage, typeof(AddOrUpdatePlayer));
    }
}

AppShell.xaml

<?xml version="1.0" encoding="UTF-8" ?>
<Shell
    x:Class="MauiUI.AppShell"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:MauiUI"
    xmlns:pages="clr-namespace:MauiUI.Pages"
    Shell.FlyoutBehavior="Disabled">

    <ShellContent
        Title="Amazons of Volleyball"
        ContentTemplate="{DataTemplate local:MainPage}"
        Route="HomePage" />

</Shell>

Still not reading a token successfully. But looking at the output I can see that Thread #8 started (might have some impact). Now I am getting a new error:

System.NotImplementedException: 'Either set MainPage or override CreateWindow.'

any idea? thnx

Wasyster
  • 2,279
  • 4
  • 26
  • 58
  • Why do you need it in the constructor? Would it be feasible to you to load the token before calling the constructor of your class and pass it in? – Julian Jan 16 '23 at 21:19
  • No, because I need to to check if I have a token. – Wasyster Jan 16 '23 at 21:22
  • Please explain the scenario. What exactly are you trying to do? Since you already know that you cannot await anything in a constructor, this might be a hint to reconsider the design of what you're building. Constructors should not be load-heavy, if possible. Wrapping the async/await in a `Task.Run()` statement just moves the operation to the thread pool, but still returns an awaitable `Task`. You won't be able to solve it that way. Either load the token before construction or use a blocking call, which is not recommended: `var jwt = SecureStorage.Default.GetAsync<...>(...).Result`. – Julian Jan 16 '23 at 21:31
  • Alternatively (possibly better and still not recommended), you could also do `var jwt = SecureStorage.Default.GetAsync<...>(...).GetAwaiter().GetResult();` which is still a blocking/synchronous call in the constructor. – Julian Jan 16 '23 at 21:37
  • My suggestion would be to move the loading of the token out of the constructor and then pass the loaded token into the constructor instead. There's an answer to a similar problem here, already: https://stackoverflow.com/a/12520574/4308455 – Julian Jan 16 '23 at 21:42
  • @ewerspej Itried, not worked – Wasyster Jan 17 '23 at 11:52
  • Can you show what you've tried in your question? – Julian Jan 17 '23 at 11:54
  • @ewerspej updated the question with mode code – Wasyster Jan 17 '23 at 11:56
  • Can you show your **AppShell.xaml**? – Julian Jan 17 '23 at 22:03

1 Answers1

3

Instead of loading the token directly inside the constructor, you should defer it to an asynchronous method which you can still call from within the constructor, but you cannot await it and you shouldn't be using a blocking call, either.

You could do this in the App.xaml.cs, which is a common place for loading data when an app starts. While the data is loading, you can set the MainPage object to some type of loading page and once the token was loaded, you can set the MainPage to the AppShell or a LoginPage instance, e.g. like this:

public App()
{
    InitializeComponent();

    MainPage = new LoadingPage();

    Load();
}

private async void Load()
{
    if (await LoadTokenAsync())
    {
        MainPage = new AppShell();
    }
    else
    {
        MainPage = new LoginPage();
    }
}

private async Task<bool> LoadTokenAsync()
{
    var jwt = await StorageService.Secure.GetAsync<JWTokenModel>(StorageKeys.Secure.JWT);

    return !(jwt is null || jwt?.Expiration < DateTime.Now);
}

This is just to demonstrate how you could solve the problem. You shouldn't do this in AppShell.xaml.cs but rather in the App.xaml.cs. Obviously, you'll need to store the token in memory somewhere.

Julian
  • 5,290
  • 1
  • 17
  • 40
  • tnx, this makse sense, how I missed this aproach. Will try. – Wasyster Jan 17 '23 at 13:29
  • no, and I updated the question – Wasyster Jan 17 '23 at 21:59
  • You should to do the loading in **App.xaml.cs** as I described in my answer. I don't think it's a good idea to do that in **AppShell.xaml.cs**. – Julian Jan 17 '23 at 22:01
  • did. Update the question under update2. – Wasyster Jan 17 '23 at 22:33
  • As I wrote in my answer already, "while the data is loading, you can set the MainPage object to some type of loading page". This is required. For potentially long loading operations you need to have some page set already as the `MainPage`. Therefore, I suggested a loading page that you can show while the app is starting up. – Julian Jan 18 '23 at 06:56
  • Yes, I missed that part. – Wasyster Jan 18 '23 at 08:45
  • Would a splash screen be enough for example? – Wasyster Jan 18 '23 at 10:16
  • Not sure what you mean by splash screen in this instance. You need to have a MainPage set within a few seconds of app start, this is especially important on iOS as the app will otherwise be terminated by the operating system. It can be any kind of page, as long as it is a `ContentPage` or `NavigationPage` that is assigned to `MainPage`. A splash screen using the `MauiSplashScreen` build action does not suffice. – Julian Jan 18 '23 at 10:48
  • I got it, I need a content page that will act as a SplashScreen. – Wasyster Jan 18 '23 at 11:06
  • it's working now, but the LoadingPage() never displays, and I wanted to use it as a splash screen. – Wasyster Jan 18 '23 at 18:46
  • Well, like I mentioned earlier, one thing is the `MauiSplashScreen` build action where you can define a basic splash screen which is shown while the app is starting up. Then there's the time it takes for your data to load. To bridge that gap between the actual splash screen and the loading of your data, you need to set some Page to the `MainPage` object because it cannot be unset or `null`. If you want your loading screen (Splash Screen as you called it) to appear a little longer, you can always add a timer or delay before setting the `MainPage` to an actual Page. – Julian Jan 18 '23 at 18:58
  • I used Thread.Sleep on MainThred, and that not worked. What worked is MainThread.InvokeOnMainThreadAsync( async () => { await Task.Delay(2000); await SetStartPage(); }); – Wasyster Jan 18 '23 at 19:13