'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.



Solution 1:[1]

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.

Solution 2:[2]

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();
        }
    }
}

Solution 3:[3]

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

Solution 4:[4]

How about using Javascript?

@inject IJSRuntime JSRuntime ......

// go back in browser using Javascript on a Razor Page 
private async void GoBack()
{
    await JSRuntime.InvokeVoidAsync("history.back");
}

Solution 5:[5]

This is the extension of this answer

The correct implementation of PageHistoryState will be using Stack. But you have to remember, that you have to add some starting point, it means that you should AddPageToHistory at your first page load, it could me MainLayout. instead of that, you will be redirected to the error page:

public class PageHistoryState
{
    private Stack<string> previousPages;
    private Stack<string> nextPages;
    private readonly string errorPageUrl;
    public PageHistoryState()
    {
        previousPages = new Stack<string>();
        nextPages = new Stack<string>();
        errorPageUrl = "/errorPage";
    }

    public void AddPageToHistory(string pageName)
    {
        previousPages.Push(pageName);
    }

    public string GetGoBackPage()
    {
        //this condition is to check if it is the first loaded page "/"
        if (previousPages.TryPeek(out string url) && !string.IsNullOrWhiteSpace(url))
        {
            //If moved to the next page check
            if (previousPages.Count > 1)
            {
                //pop the current page
                nextPages.Push(previousPages.Pop());
                //pop the previous page -> "/"
                url = previousPages.Pop();
                return url;
            }
        }

        //If stack is empty redirect to the error page
        return errorPageUrl;
    }

    public string GetGoForwardPage()
    {
        if (nextPages.TryPop(out string url) && !string.IsNullOrWhiteSpace(url))
            return url;

        //If stack is empty redirect to the error page
        return errorPageUrl;
    }

    public bool CanGoForward() => nextPages.Any();
    public bool CanGoBack() => previousPages.Count > 1;
}

Solution 6:[6]

Navigator Class

 public class Navigator
    {
        string _currentPage = "/";
        string _homePage { get; set; } = "/";
        NavigationManager Navigation { get; set; }
        IJSRuntime JS { get; set; }
        public void SetNavigation(NavigationManager navigation, IJSRuntime js, string dfaultPage = "/")
        {
            this.Navigation = navigation;
            this.JS = js;
            _currentPage = dfaultPage;
            _homePage = _currentPage;
        }
        public Navigator()
        {
            
        }
        public Stack<string> Navigators = new Stack<string>();
        public void NavigateTo(string parameter)
        {
            if (parameter != _currentPage)
            {
                Navigators.Push(parameter);
                Navigation?.NavigateTo(parameter);
                _currentPage = parameter;
            }
        }
        public async void ExecuteCommand(NavigateType code, string param)
        {
            if (code ==  NavigateType.Back)
            {
                if (Navigators.Count >= 1)
                {
                    string nav = null;
                    if(Peek() == param)
                        Navigators.Pop();
                    if(Navigators.Count >=1)
                      nav = Navigators.Pop();
                    if (nav != null)
                        _currentPage = nav;
                    else
                    {
                        _currentPage = _homePage;
                    }
                    await JS.InvokeVoidAsync("history.back");
                    BackPressed?.Invoke(this, nav);
                }
                else
                {
                   
                    _currentPage = _homePage;
                    await JS.InvokeVoidAsync("history.back");
                    BackPressed?.Invoke(this, null);
                   
                }
                
            }
            else if (code == NavigateType.Navigate)
            {
                if (param != _currentPage)
                {
                    _currentPage = param;
                    Navigators.Push(param);
                    Navigation?.NavigateTo(param);
                    Navigated?.Invoke(this, param);
                }

            }
        }
        public string Peek()
        {
            return Navigators.Peek();
        }
        public string Pop()
        {
            return Navigators.Pop();
        }
        public event EventHandler<string> BackPressed;
        public event EventHandler<string> Navigated;
    }
    public enum NavigateType
    {
        Navigate,
        Back,

    }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Diogo Neves
Solution 2 Pavel Melnikov
Solution 3 Dharman
Solution 4
Solution 5
Solution 6 user18763174