Resolving pages programatically in NotFound Blazor Router instead using @page directive reload client

I'm able to load pages dinamically using a CustomRouteLoader that is using as FragmentCode on a NotFound property of the Router tag in the App.razor file. This CustomRouteLoader use a CustomRouteService to get the Type of the razor component that must use as page to load for a not found URL path. (You can see the solution of MrC aka Shaun Curtis in this question: Register pages programatically in Blazor instead using @page directive using LazyAssemblyLoader)

The problem that seams to happends when the page is resolved from NotFound instead of be resolved by the Found in the Router. When a page contains @page "" works fine and the client is not reloaded, but when the page is resolved by NotFound Route way, the client is reload (it calls Program.cs code again).

To emulate this behavior with a more simple code I create a "WebAssembly of Blazor Application", cheking "Hosted ASP.NET Core" and "Progressive web application", and I do the next changes:

I added CustomRouteService.cs:

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

namespace TestResolvePage.Client
{
    public class CustomRouteService
    {
        public CustomRouteService(IEnumerable<CustomRouteData> pages)
        {
            _pages.AddRange(pages);
        }

        public bool IsReloaded { get; set; }

        private List<CustomRouteData> _pages = new List<CustomRouteData>();

        public IEnumerable<CustomRouteData> Pages => _pages.AsEnumerable();

        public bool TryAddRoute(CustomRouteData data)
        {
            var isComponent = typeof(IComponent).IsAssignableFrom(data.Page);
            var newRoute = !_pages.Any(item => item.Route.Equals(data.Route, StringComparison.CurrentCultureIgnoreCase));

            if (isComponent && newRoute)
            {
                _pages.Add(data);
                return true;
            }

            return false;
        }

        public bool TryAddRoute(string route, string className)
        {
            var page = Type.GetType(className);
            if (page is null)
                return false;

            return this.TryAddRoute(new CustomRouteData(route, page));
        }

        public bool TryAddRoute(string route, string libraryName, string className)
        {
            var page = Type.GetType($"{className}, {libraryName}");
            if (page is null)
                return false;

            return this.TryAddRoute(new CustomRouteData(route, page));
        }

        public bool TryGetRoute(string route, out Type? page)
        {
            page = null;
            var data = _pages.FirstOrDefault(item => item.Route.Equals(route, StringComparison.CurrentCultureIgnoreCase));
            if (data is not null)
                page = data.Page;

            return page != null;
        }
    }

    public record CustomRouteData(string Route, Type Page);
}

I added CustomRouteLoader.razor:

@if (_isRoute)
{
    <RouteView RouteData=_routeData/>@*efaultLayout="@typeof(MainLayout)" />*@
}
else
{
    @this.NotFound
}


@code {
    [Parameter] public RenderFragment? NotFound { get; set; }

    [Inject] public NavigationManager NavManager { get; set; } = default!;
    [Inject] public CustomRouteService PageService { get; set; } = default!;

    //    private Type? _component;
    private bool _isRoute;
    private RouteData? _routeData;

    protected override void OnParametersSet()
    {
        _routeData = null;

        // Get rhe route Uri
        var url = this.NavManager.Uri.Replace(NavManager.BaseUri, "/");

        // Try to get the component associated with the route
        PageService.TryGetRoute(url, out Type? _component);

        // Check we have a type and it implements IComponent
        _isRoute = _component is not null && typeof(Microsoft.AspNetCore.Components.IComponent).IsAssignableFrom(_component);

        // Create the RouteData
        if (_isRoute && _component is not null)
            _routeData = new RouteData(_component, new Dictionary<string, object>());
    }

}

I modified Program.cs adding next lines before await builder.Build().RunAsync();:

CustomRouteService routeService = new CustomRouteService(new List<CustomRouteData> { new CustomRouteData("/page-with-no-attribute", typeof(TestResolvePage.Client.Pages.PageWithNoAttribute)) });
builder.Services.AddSingleton<CustomRouteService>(sp => routeService);
routeService.IsReloaded = true;

I changed App.razor like this (noticed that I remove both Layouts):

<Router AppAssembly="@typeof(App).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData"/>@* DefaultLayout="@typeof(MainLayout)"/>*@
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
    <NotFound>
        <CustomRouteLoader>
            <NotFound>
                <PageTitle>Not found</PageTitle>
                <LayoutView >@*Layout="@typeof(MainLayout)"/>*@
                    <p role="alert">Sorry, there's nothing at this address.</p>
                </LayoutView>
            </NotFound>
        </CustomRouteLoader>
    </NotFound>
</Router>

I changed Counter.razor, FetchData.razor and Index.razor to add the next line:

@layout MainLayout

I added into Pages folder PageWithAttribute.razor:

@page "/page-with-attribute"

@inject TestResolvePage.Client.CustomRouteService CustomRouteService

<PageTitle>Example of a Page with an Attribute @@page</PageTitle>

<h1>Example of a Page with an Attribute @@page</h1>

<p role="status">
    @if (CustomRouteService.IsReloaded)
    {
        @("The singleton class was reloaded because the page is recharged like if I press F5 in the browser(the Program.cs is executed again before arreive here)")
    }
    else
    {
        @("The singleton class was the same because the page is not recharged(the Program.cs is executed again before arreive here)")
    }
</p>

I added into Pages folder PageWithNoAttribute.razor:

@inject TestResolvePage.Client.CustomRouteService CustomRouteService

<PageTitle>Example of a Page with no Attribute @@page</PageTitle>

<h1>Example of a Page with no Attribute @@page</h1>

<p role="status">
    @if (CustomRouteService.IsReloaded)
    {
        @("The singleton class was reloaded because the page is recharged like if I press F5 in the browser(the Program.cs is executed again before arreive here)")
    }
    else
    {
        @("The singleton class was the same because the page is not recharged(the Program.cs is executed again before arreive here)")
    }
</p>

And finally I added 2 entries into the NavMenu.razor:

<div class="nav-item px-3">
    <NavLink class="nav-link" href="page-with-attribute">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Page with Atribute
    </NavLink>
</div>
<div class="nav-item px-3">
    <NavLink class="nav-link" href="page-with-no-attribute">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Page with No Atribute
    </NavLink>
</div>

EDIT: I continue doing tests found somthing strange behavior.

TEST 1: I resotre the App.razor to the original value for NotFound section:

<Router AppAssembly="@typeof(App).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData"/>
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
    <NotFound>
        <PageTitle>Not found</PageTitle>
        <LayoutView>
            <p role="alert">Sorry, there's nothing at this address.</p>
        </LayoutView>

    </NotFound>
</Router>

and I browse to a page path that not exits, and then I can see that the Loading page is showed a short time but the code of Program.cs is not called.

TEST 2: I move the code of the "original" NotFound section of the App.razor to a new file PageLikeAppNotFoundSection.razor and I refer this new file in the NotFoundSection of App.razor.

The PageLikeAppNotFoundSection.razor file:

<PageTitle>Not found</PageTitle>
<LayoutView>
    <p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>

The App.razor file:

<Router AppAssembly="@typeof(App).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData"/>
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
    <NotFound>
        <TestResolvePage.Client.Pages.PageLikeAppNotFoundSection/>
    </NotFound>
</Router>

This code do that loading page were showed and the code of Program.cs is called.

Why is this?

EDIT 2: If I add an buton and I call in the onclick event to the NavigateTo of the NavigationManager in order to go to the same page, the behavior now is different, it works like I expected without reloading the cliente and no calling the code of Program.cs:

<div class="nav-item px-3">
    <NavLink class="nav-link" href="page-with-no-attribute">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Page with No Atribute
    </NavLink>
</div>
<button class="btn btn-dark" @onclick=NavigateToTheSamePage>Navigate to Page with No Atribute</button>
...
@code {
    ...
    private void NavigateToTheSamePage()
    {
        navManager.NavigateTo("/page-with-no-attribute");
    }
    ...
}

Upvotes: 2

Views: 450

Answers (1)

I FOUND A SOLUTION. I navigate to the code of the Router class and I copy the code into my own namespace. Also I copied all the clases that fails because properties are inaccesible because there are not public. Once it compiles, I search the RouteAttribute and in the method Create that returns RouteTable, I modify the Dictionary to add my custom routes.

The result is this (I only copy the body of the RouteTableFactory class because I can write more than 30000 characters:

using System;
using System.CodeDom.Compiler;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using System.Reflection.Emit;
using System.Reflection.Metadata;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.HotReload;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.Extensions.Logging;

namespace TestResolvePage.Client.Routing
{
     /// <summary>
     /// A component that supplies route data corresponding to the current navigation state.
     /// </summary>
    public class Router 

    internal readonly struct RouteKey 

    internal sealed class RouteTable

    [DebuggerDisplay("Handler = {Handler}, Template = {Template}")]
    internal sealed class RouteEntry

    [DebuggerDisplay("{TemplateText}")]
    internal sealed class RouteTemplate

    internal sealed class RouteContext

    internal sealed class TemplateSegment

    internal sealed class HotReloadManager

    /// <summary>
    /// Resolves components for an application.
    /// </summary>
    internal static class RouteTableFactory
    {
        private static readonly ConcurrentDictionary<RouteKey, RouteTable> Cache = new ConcurrentDictionary<RouteKey, RouteTable>();

        public static readonly IComparer<RouteEntry> RoutePrecedence = Comparer<RouteEntry>.Create(new Comparison<RouteEntry>(RouteComparison));

        public static RouteTable Create(RouteKey routeKey)
        {
            if (Cache.TryGetValue(routeKey, out var value))
            {
                return value;
            }
            RouteTable routeTable = Create(GetRouteableComponents(routeKey));
            Cache.TryAdd(routeKey, routeTable);
            return routeTable;
        }

        public static void ClearCaches()
        {
            Cache.Clear();
        }

        [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Application code does not get trimmed, and the framework does not define routable components.")]
        private static List<Type> GetRouteableComponents(RouteKey routeKey)
        {
            List<Type> list = new List<Type>();
            if ((object)routeKey.AppAssembly != null)
            {
                GetRouteableComponents(list, routeKey.AppAssembly);
            }
            if (routeKey.AdditionalAssemblies != null)
            {
                foreach (Assembly item in routeKey.AdditionalAssemblies!)
                {
                    if (item != routeKey.AppAssembly)
                    {
                        GetRouteableComponents(list, item);
                    }
                }
                return list;
            }
            return list;
            static void GetRouteableComponents(List<Type> routeableComponents, Assembly assembly)
            {
                foreach (Type exportedType in assembly.ExportedTypes)
                {
                    if (typeof(IComponent)!.IsAssignableFrom(exportedType) && exportedType.IsDefined(typeof(RouteAttribute)))
                    {
                        routeableComponents.Add(exportedType);
                    }
                }
            }
        }

        internal static RouteTable Create(List<Type> componentTypes)
        {
            Dictionary<Type, string[]> dictionary = new Dictionary<Type, string[]>();
            foreach (Type componentType in componentTypes)
            {
                object[] customAttributes = componentType.GetCustomAttributes(typeof(RouteAttribute), inherit: false);
                string[] array = new string[customAttributes.Length];
                for (int i = 0; i < customAttributes.Length; i++)
                {
                    RouteAttribute routeAttribute = (RouteAttribute)customAttributes[i];
                    array[i] = routeAttribute.Template;
                }
                dictionary.Add(componentType, array);
            }
            /// REGISTRATION OF THE ADDITIONAL PAGES WTIH NO NEED TO INSERT @page (RouteAttribute) ///
            dictionary.Add(typeof(TestResolvePage.Client.Pages.PageWithNoAttribute), new string[] { "/page-with-no-attribute" });
            return Create(dictionary);
        }

        [UnconditionalSuppressMessage("Trimming", "IL2067", Justification = "Application code does not get trimmed, and the framework does not define routable components.")]
        internal static RouteTable Create(Dictionary<Type, string[]> templatesByHandler)
        {
            List<RouteEntry> list = new List<RouteEntry>();
            foreach (KeyValuePair<Type, string[]> item4 in templatesByHandler)
            {
                item4.Deconstruct(out var key, out var value);
                Type handler = key;
                string[] array = value;
                HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
                (RouteTemplate, HashSet<string>)[] array2 = new (RouteTemplate, HashSet<string>)[array.Length];
                for (int i = 0; i < array.Length; i++)
                {
                    RouteTemplate routeTemplate = TemplateParser.ParseTemplate(array[i]);
                    HashSet<string> parameterNames = GetParameterNames(routeTemplate);
                    array2[i] = (routeTemplate, parameterNames);
                    foreach (string item5 in parameterNames)
                    {
                        hashSet.Add(item5);
                    }
                }
                (RouteTemplate, HashSet<string>)[] array3 = array2;
                for (int j = 0; j < array3.Length; j++)
                {
                    (RouteTemplate, HashSet<string>) tuple = array3[j];
                    RouteTemplate item = tuple.Item1;
                    HashSet<string> item2 = tuple.Item2;
                    List<string> unusedParameterNames = GetUnusedParameterNames(hashSet, item2);
                    RouteEntry item3 = new RouteEntry(item, handler, unusedParameterNames);
                    list.Add(item3);
                }
            }
            list.Sort(RoutePrecedence);
            return new RouteTable(list.ToArray());
        }

        private static HashSet<string> GetParameterNames(RouteTemplate routeTemplate)
        {
            HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            TemplateSegment[] segments = routeTemplate.Segments;
            foreach (TemplateSegment templateSegment in segments)
            {
                if (templateSegment.IsParameter)
                {
                    hashSet.Add(templateSegment.Value);
                }
            }
            return hashSet;
        }

        private static List<string> GetUnusedParameterNames(HashSet<string> allRouteParameterNames, HashSet<string> routeParameterNames)
        {
            List<string> list = null;
            foreach (string allRouteParameterName in allRouteParameterNames)
            {
                if (!routeParameterNames.Contains(allRouteParameterName))
                {
                    if (list == null)
                    {
                        list = new List<string>();
                    }
                    list.Add(allRouteParameterName);
                }
            }
            return list;
        }

        /// <summary>
        /// Route precedence algorithm.
        /// We collect all the routes and sort them from most specific to
        /// less specific. The specificity of a route is given by the specificity
        /// of its segments and the position of those segments in the route.
        /// * A literal segment is more specific than a parameter segment.
        /// * A parameter segment with more constraints is more specific than one with fewer constraints
        /// * Segment earlier in the route are evaluated before segments later in the route.
        /// For example:
        /// /Literal is more specific than /Parameter
        /// /Route/With/{parameter} is more specific than /{multiple}/With/{parameters}
        /// /Product/{id:int} is more specific than /Product/{id}
        ///
        /// Routes can be ambiguous if:
        /// They are composed of literals and those literals have the same values (case insensitive)
        /// They are composed of a mix of literals and parameters, in the same relative order and the
        /// literals have the same values.
        /// For example:
        /// * /literal and /Literal
        /// /{parameter}/literal and /{something}/literal
        /// /{parameter:constraint}/literal and /{something:constraint}/literal
        ///
        /// To calculate the precedence we sort the list of routes as follows:
        /// * Shorter routes go first.
        /// * A literal wins over a parameter in precedence.
        /// * For literals with different values (case insensitive) we choose the lexical order
        /// * For parameters with different numbers of constraints, the one with more wins
        /// If we get to the end of the comparison routing we've detected an ambiguous pair of routes.
        /// </summary>
        internal static int RouteComparison(RouteEntry x, RouteEntry y)
        {
            if (x == y)
            {
                return 0;
            }
            RouteTemplate template = x.Template;
            RouteTemplate template2 = y.Template;
            int num = Math.Min(template.Segments.Length, template2.Segments.Length);
            int num2 = 0;
            for (int i = 0; i < num; i++)
            {
                TemplateSegment templateSegment = template.Segments[i];
                TemplateSegment templateSegment2 = template2.Segments[i];
                int rank = GetRank(templateSegment);
                int rank2 = GetRank(templateSegment2);
                num2 = rank.CompareTo(rank2);
                int num3 = rank;
                int num4 = rank2;
                if (num3 == 0 && num4 == 0)
                {
                    num2 = StringComparer.OrdinalIgnoreCase.Compare(templateSegment.Value, templateSegment2.Value);
                }
                if (num2 != 0)
                {
                    break;
                }
            }
            if (num2 == 0)
            {
                num2 = template.Segments.Length.CompareTo(template2.Segments.Length);
            }
            if (num2 == 0)
            {
                DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(57, 4);
                defaultInterpolatedStringHandler.AppendLiteral("The following routes are ambiguous:\r\n'");
                defaultInterpolatedStringHandler.AppendFormatted(x.Template.TemplateText);
                defaultInterpolatedStringHandler.AppendLiteral("' in '");
                defaultInterpolatedStringHandler.AppendFormatted(x.Handler.FullName);
                defaultInterpolatedStringHandler.AppendLiteral("'\r\n'");
                defaultInterpolatedStringHandler.AppendFormatted(y.Template.TemplateText);
                defaultInterpolatedStringHandler.AppendLiteral("' in '");
                defaultInterpolatedStringHandler.AppendFormatted(y.Handler.FullName);
                defaultInterpolatedStringHandler.AppendLiteral("'\r\n");
                throw new InvalidOperationException(defaultInterpolatedStringHandler.ToStringAndClear());
            }
            return num2;
        }

        private static int GetRank(TemplateSegment xSegment)
        {
            if (xSegment != null)
            {
                if (!xSegment.IsParameter)
                {
                    return 0;
                }
                if (!xSegment.IsCatchAll)
                {
                    UrlValueConstraint[] constraints = xSegment.Constraints;
                    if (constraints != null)
                    {
                        if (constraints.Length > 0)
                        {
                            return 1;
                        }
                        return 2;
                    }
                }
                else
                {
                    UrlValueConstraint[] constraints = xSegment.Constraints;
                    if (constraints != null)
                    {
                        if (constraints.Length > 0)
                        {
                            return 3;
                        }
                        return 4;
                    }
                }
            }
            DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(29, 1);
            defaultInterpolatedStringHandler.AppendLiteral("Unknown segment definition '");
            defaultInterpolatedStringHandler.AppendFormatted(xSegment);
            defaultInterpolatedStringHandler.AppendLiteral(".");
            throw new InvalidOperationException(defaultInterpolatedStringHandler.ToStringAndClear());
        }
    }

    /// <summary>
    /// Shared logic for parsing tokens from route values and querystring values.
    /// </summary>
    internal abstract class UrlValueConstraint

    /// <summary>
    /// Provides information about the current asynchronous navigation event
    /// including the target path and the cancellation token.
    /// </summary>
    public sealed class NavigationContext

    internal static class RouteConstraint

    internal sealed class TemplateParser

    internal struct StringSegmentAccumulator

    [AttributeUsage(AttributeTargets.Method)]
    public sealed class LoggerMessageAttribute 
}

The important line is this:

/// REGISTRATION OF THE ADDITIONAL PAGES WTIH NO NEED TO INSERT @page (RouteAttribute) ///
dictionary.Add(typeof(TestResolvePage.Client.Pages.PageWithNoAttribute), new string[] { "/page-with-no-attribute" });

Upvotes: 0

Related Questions