7
votes

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.

3

3 Answers

11
votes

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.

4
votes

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

2
votes

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