AvaloniaInside / Shell

Reduces the complexity of mobile/desktop application development by providing the fundamental features that most applications require
MIT License
180 stars 11 forks source link

Persisting navigation state #26

Closed gentledepp closed 1 week ago

gentledepp commented 4 months ago

AvaloniaUI has a tutorial (and even a docs page) on how to persist UI state with ReactiveUI

Is there any way we can actually persist the routing state of the Shell like this too?

How do you solve this in your projects? I'd be very interested to learn :-)

OmidID commented 4 months ago

Hello @gentledepp It is possible. I already did this in my project. Basically the shell is very UI native and to handle the application architecture is up to the developer.


public class ModelBasePage<T> : Page
    where T : ViewModelBase
{
    public T ViewModel { get; set; }

    protected override Type StyleKeyOverride => typeof(Page);

    private CancellationTokenSource? _cancellationTokenSource;
    protected CancellationToken? PageLifetimeCancellationToken => _cancellationTokenSource?.Token;

    private void KillToken()
    {
        try { _cancellationTokenSource?.Cancel(); } catch { }
        try { _cancellationTokenSource?.Dispose(); } catch { }
    }

    protected override void OnDataContextChanged(EventArgs e)
    {
        if (DataContext is T viewModel)
        {
            ViewModel = viewModel;
            viewModel.LifetimeCancellationToken = PageLifetimeCancellationToken;
            ViewModel.Navigator = Navigator;
        }
        base.OnDataContextChanged(e);
    }

    public override async Task InitialiseAsync(CancellationToken cancellationToken)
    {
        KillToken();
        _cancellationTokenSource = new CancellationTokenSource();

        if (DataContext is T viewModel)
        {
            ViewModel = viewModel;
        }
        else
        {
            DataContext = ViewModel = Locator.Current.GetService<T>() ?? throw new KeyNotFoundException("Cannot find ViewModel");
        }
        ViewModel.LifetimeCancellationToken = PageLifetimeCancellationToken;
        ViewModel.Navigator = Navigator;

        await base.InitialiseAsync(_cancellationTokenSource.Token);
        await ViewModel.InitializeAsync(_cancellationTokenSource.Token);
    }

    public override Task ArgumentAsync(object args, CancellationToken cancellationToken)
    {
        if (DataContext is ViewModelBase viewModel)
            return viewModel.HandleParameter(args, _cancellationTokenSource?.Token ?? cancellationToken);

        return base.ArgumentAsync(args, cancellationToken);
    }

    public override Task TerminateAsync(CancellationToken cancellationToken)
    {
        KillToken();
        return base.TerminateAsync(cancellationToken);
    }
}

And this is the class for ViewModelBase:


public class ViewModelBase : ReactiveObject
{
    public enum ViewModelStatus
    {
        Initialize,
        Starting,
        Started,
        Closed
    }

    public INavigator? Navigator { get; set; }

    public object? Parameter { get; set; }

    [Reactive] public ViewModelStatus Status { get; private set; } = ViewModelStatus.Initialize;
    public CancellationToken? LifetimeCancellationToken { get; internal set; }

    public async Task InitializeAsync(CancellationToken cancellationToken)
    {
        if (Status != ViewModelStatus.Initialize) return;

        Status = ViewModelStatus.Starting;
        await StartAsync(cancellationToken);
        Status = ViewModelStatus.Started;
    }

    public Task HandleParameter(object parameter, CancellationToken cancellationToken)
    {
        Parameter = parameter;
        return ParameterAsync(parameter, cancellationToken);
    }

    protected virtual Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;

    protected virtual Task ParameterAsync(object parameter, CancellationToken cancellationToken) => Task.CompletedTask;

    protected T GetParameter<T>() => Parameter is T cast ? cast : default;
}
gentledepp commented 4 months ago

Thanks for your answer, but I think we are talking about different things.

I was asking for being able to persist navigation state.

So like: in our app, we started in the Main page, then nagivated to Page2 and Page3. Then the app is suspended. When it is continued, I would like to load that navigation state back into the shell so the user can still navigate back to page 2 and the Main page, still having all ViewModels state persisted (like the search string or whatever)

So what I would need is:

OmidID commented 4 months ago

OK. What I understand you need to navigate from home page to page1 then page 2 and then back to home page with some argument. Possible! Please check the Navigate method from INavigator and you have navigate type and you can pass Clear type. You can find list of nav types here: https://github.com/AvaloniaInside/Shell/blob/main/src/AvaloniaInside.Shell/NavigateType.cs

In case if you want to send value you can pass it as argument.

in this way, the instance of your main page will remain same.

The navigation stack keep the instances of your pages until it remove from stack. Very similar to mobile navigation stack.

Navigator.NavigateAsync("/main", NavigateType.Clear, "Value 1 selected", cancellationToken);