Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Blazor Navigation Manager Go Back?

While using blazor, I want to be able to "go back" to a page I was before.

I found this issue and looks like it's a dead end?

This feature is something so basic, that I can't believe it doesn't exits.

Is there any walk around to make this "go back" functionality ?

Please notice that I cannot use window.goBack or history.goBack because my app doesn't create any history, and also shouldn't create any history.

The only way to "create" a history is to use the forceLoad option of Navigation.NavigateTo but if I do, it will try to load my entire app again, which is slow and I don't want to.

like image 301
Vencovsky Avatar asked Jun 24 '20 18:06

Vencovsky


3 Answers

What you need is a page history state manager:

For the following example I'm using Blazor Wasm but you can use this example in Blazor Server as well.

In the client app I've added this class: PageHistoryState:

 public class PageHistoryState
    {
        private List<string> previousPages;

        public PageHistoryState()
        {
            previousPages = new List<string>();
        }
        public void AddPageToHistory(string pageName)
        {
            previousPages.Add(pageName);
        }

        public string GetGoBackPage()
        {
            if (previousPages.Count > 1)
            {
                // You add a page on initialization, so you need to return the 2nd from the last
                return previousPages.ElementAt(previousPages.Count - 2);
            }

            // Can't go back because you didn't navigate enough
            return previousPages.FirstOrDefault();
        }

        public bool CanGoBack()
        {
            return previousPages.Count > 1;
        }
    }

Then add this class to the services as a singleton:

builder.Services.AddSingleton<PageHistoryState>();

Inject it in your pages:

@inject WasmBlazor.Client.PageHistoryState PageHistoryState

In my markup then I've check to see if I can go back a page:

@if (PageHistoryState.CanGoBack())
{
    <a href="@PageHistoryState.GetGoBackPage()">Go Back</a>
}

And I've overwritten OnInitialized()

protected override void OnInitialized()
{
    PageHistoryState.AddPageToHistory("/counter");
    base.OnInitialized();
}

I've done the same thing in the "fetch data" page, and I'm able to go back without the need of the JSInterop.

like image 52
Diogo Neves Avatar answered Nov 15 '22 23:11

Diogo Neves


I ended up with a bit improved solution that wraps/incapsulates the NavigationManager, keeps everything in a single place and does not depend on Pages or something else. It also keeps the history buffer size in some reasonable range.

Navigation.cs

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;

namespace MyApp
{
    public class Navigation : IDisposable
    {
        private const int MinHistorySize = 256;
        private const int AdditionalHistorySize = 64;
        private readonly NavigationManager _navigationManager;
        private readonly List<string> _history;

        public Navigation(NavigationManager navigationManager)
        {
            _navigationManager = navigationManager;
            _history = new List<string>(MinHistorySize + AdditionalHistorySize);
            _history.Add(_navigationManager.Uri);
            _navigationManager.LocationChanged += OnLocationChanged;
        }

        /// <summary>
        /// Navigates to the specified url.
        /// </summary>
        /// <param name="url">The destination url (relative or absolute).</param>
        public void NavigateTo(string url)
        {
            _navigationManager.NavigateTo(url);
        }

        /// <summary>
        /// Returns true if it is possible to navigate to the previous url.
        /// </summary>
        public bool CanNavigateBack => _history.Count >= 2;

        /// <summary>
        /// Navigates to the previous url if possible or does nothing if it is not.
        /// </summary>
        public void NavigateBack()
        {
            if (!CanNavigateBack) return;
            var backPageUrl = _history[^2];
            _history.RemoveRange(_history.Count - 2, 2);
            _navigationManager.NavigateTo(backPageUrl);
        }

        // .. All other navigation methods.

        private void OnLocationChanged(object sender, LocationChangedEventArgs e)
        {
            EnsureSize();
            _history.Add(e.Location);
        }

        private void EnsureSize()
        {
            if (_history.Count < MinHistorySize + AdditionalHistorySize) return;
            _history.RemoveRange(0, _history.Count - MinHistorySize);
        }

        public void Dispose()
        {
            _navigationManager.LocationChanged -= OnLocationChanged;
        }
    }
}

Then you can add this class to dependency injection as a singleton service and initialise.

Program.cs

using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace MyApp
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("app");

            builder.Services.AddSingleton<Navigation>();
            // .. other services.
            
            var host = builder.Build();
            await Initialize(host);
            await host.RunAsync();
        }

        private static async Task Initialize(WebAssemblyHost host)
        {
            host.Services.GetService<Navigation>();
            // .. other initialization calls.
        }
    }
}

After that you can use it in any place you want using the Inject directive/attribute.

SomePage.cshtml

@page "/SomePage"
@inject Navigation Navigation

<h3>SomePage</h3>

<button @onclick="NavigateBackClick">Navigate Back</button>

@code {
    private void NavigateBackClick()
    {
        Navigation.NavigateBack();
    }
}

SomeService.cs

namespace MyApp
{
    public class SomeService
    {
        private readonly Navigation _navigation;

        public SomeService(Navigation navigation)
        {
            _navigation = navigation;
        }

        public void SomeMethod()
        {
            // ...
            _navigation.NavigateBack();
        }
    }
}
like image 10
Pavel Melnikov Avatar answered Nov 15 '22 23:11

Pavel Melnikov


I modified Diogo's answer above into what I feel is a much more elegant solution.

First, create a BasePageComponent.cs class, which inherits from the ComponentBase class:

// Using statements/namespace go here

[Authorize]
public class BasePageComponent: ComponentBase
{
    [Inject]
    protected NavigationManager _navManager { get; set; }
    [Inject]
    protected PageHistoryState _pageState { get; set; }

    public BasePageComponent(NavigationManager navManager, PageHistoryState pageState)
    {
        _navManager = navManager;
        _pageState = pageState;
    }

    public BasePageComponent()
    {
    }

    protected override void OnInitialized()
    {
        base.OnInitialized();
        _pageState.AddPage(_navManager.Uri);
    }
}

This is what each of your pages will inherit from. It handles injecting the PageHistoryState service, as well as appending a newly navigated page. It does this all "behind the scenes" of your actual pages.

Now, in a given page, you inherit from BasePageComponent:

@page "/workouts/new"
@inherits BasePageComponent

/* ...RenderFragments/Razor view here...*/

@code {
    /* ...properties here...*/

    // This is an example of how to consume the _navManager and _pageState objects if desired, without any boilerplate code.
    private void OnCancel()
    {
        _navManager.NavigateTo(_pageState.PreviousPage());
    }
}

In my example component (stripped for brevity) it adds a new element to the page history stack with no markup besides inheriting from BasePageComponent. Taylor

like image 8
Taylor C. White Avatar answered Nov 15 '22 23:11

Taylor C. White