/
Router.cs
203 lines (169 loc) · 8.62 KB
/
Router.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Components.Routing
{
/// <summary>
/// A component that supplies route data corresponding to the current navigation state.
/// </summary>
public class Router : IComponent, IHandleAfterRender, IDisposable
{
static readonly char[] _queryOrHashStartChar = new[] { '?', '#' };
static readonly ReadOnlyDictionary<string, object> _emptyParametersDictionary
= new ReadOnlyDictionary<string, object>(new Dictionary<string, object>());
RenderHandle _renderHandle;
string _baseUri;
string _locationAbsolute;
bool _navigationInterceptionEnabled;
ILogger<Router> _logger;
[Inject] private NavigationManager NavigationManager { get; set; }
[Inject] private INavigationInterception NavigationInterception { get; set; }
[Inject] private ILoggerFactory LoggerFactory { get; set; }
/// <summary>
/// Gets or sets the assembly that should be searched for components matching the URI.
/// </summary>
[Parameter] public Assembly AppAssembly { get; set; }
/// <summary>
/// Gets or sets a collection of additional assemblies that should be searched for components
/// that can match URIs.
/// </summary>
[Parameter] public IEnumerable<Assembly> AdditionalAssemblies { get; set; }
/// <summary>
/// Gets or sets the content to display when no match is found for the requested route.
/// </summary>
[Parameter] public RenderFragment NotFound { get; set; }
/// <summary>
/// Gets or sets the content to display when a match is found for the requested route.
/// </summary>
[Parameter] public RenderFragment<RouteData> Found { get; set; }
private RouteTable Routes { get; set; }
/// <inheritdoc />
public void Attach(RenderHandle renderHandle)
{
_logger = LoggerFactory.CreateLogger<Router>();
_renderHandle = renderHandle;
_baseUri = NavigationManager.BaseUri;
_locationAbsolute = NavigationManager.Uri;
NavigationManager.LocationChanged += OnLocationChanged;
}
/// <inheritdoc />
public Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
if (AppAssembly == null)
{
throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(AppAssembly)}.");
}
// Found content is mandatory, because even though we could use something like <RouteView ...> as a
// reasonable default, if it's not declared explicitly in the template then people will have no way
// to discover how to customize this (e.g., to add authorization).
if (Found == null)
{
throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(Found)}.");
}
// NotFound content is mandatory, because even though we could display a default message like "Not found",
// it has to be specified explicitly so that it can also be wrapped in a specific layout
if (NotFound == null)
{
throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(NotFound)}.");
}
var assemblies = AdditionalAssemblies == null ? new[] { AppAssembly } : new[] { AppAssembly }.Concat(AdditionalAssemblies);
Routes = RouteTableFactory.Create(assemblies);
Refresh(isNavigationIntercepted: false);
return Task.CompletedTask;
}
/// <inheritdoc />
public void Dispose()
{
NavigationManager.LocationChanged -= OnLocationChanged;
}
private static string StringUntilAny(string str, char[] chars)
{
var firstIndex = str.IndexOfAny(chars);
return firstIndex < 0
? str
: str.Substring(0, firstIndex);
}
private void Refresh(bool isNavigationIntercepted)
{
var locationPath = NavigationManager.ToBaseRelativePath(_locationAbsolute);
locationPath = StringUntilAny(locationPath, _queryOrHashStartChar);
var context = new RouteContext(locationPath);
Routes.Route(context);
if (context.Handler != null)
{
if (!typeof(IComponent).IsAssignableFrom(context.Handler))
{
throw new InvalidOperationException($"The type {context.Handler.FullName} " +
$"does not implement {typeof(IComponent).FullName}.");
}
Log.NavigatingToComponent(_logger, context.Handler, locationPath, _baseUri);
var routeData = new RouteData(
context.Handler,
context.Parameters ?? _emptyParametersDictionary);
_renderHandle.Render(Found(routeData));
}
else
{
if (!isNavigationIntercepted)
{
Log.DisplayingNotFound(_logger, locationPath, _baseUri);
// We did not find a Component that matches the route.
// Only show the NotFound content if the application developer programatically got us here i.e we did not
// intercept the navigation. In all other cases, force a browser navigation since this could be non-Blazor content.
_renderHandle.Render(NotFound);
}
else
{
Log.NavigatingToExternalUri(_logger, _locationAbsolute, locationPath, _baseUri);
NavigationManager.NavigateTo(_locationAbsolute, forceLoad: true);
}
}
}
private void OnLocationChanged(object sender, LocationChangedEventArgs args)
{
_locationAbsolute = args.Location;
if (_renderHandle.IsInitialized && Routes != null)
{
Refresh(args.IsNavigationIntercepted);
}
}
Task IHandleAfterRender.OnAfterRenderAsync()
{
if (!_navigationInterceptionEnabled)
{
_navigationInterceptionEnabled = true;
return NavigationInterception.EnableNavigationInterceptionAsync();
}
return Task.CompletedTask;
}
private static class Log
{
private static readonly Action<ILogger, string, string, Exception> _displayingNotFound =
LoggerMessage.Define<string, string>(LogLevel.Debug, new EventId(1, "DisplayingNotFound"), $"Displaying {nameof(NotFound)} because path '{{Path}}' with base URI '{{BaseUri}}' does not match any component route");
private static readonly Action<ILogger, Type, string, string, Exception> _navigatingToComponent =
LoggerMessage.Define<Type, string, string>(LogLevel.Debug, new EventId(2, "NavigatingToComponent"), "Navigating to component {ComponentType} in response to path '{Path}' with base URI '{BaseUri}'");
private static readonly Action<ILogger, string, string, string, Exception> _navigatingToExternalUri =
LoggerMessage.Define<string, string, string>(LogLevel.Debug, new EventId(3, "NavigatingToExternalUri"), "Navigating to non-component URI '{ExternalUri}' in response to path '{Path}' with base URI '{BaseUri}'");
internal static void DisplayingNotFound(ILogger logger, string path, string baseUri)
{
_displayingNotFound(logger, path, baseUri, null);
}
internal static void NavigatingToComponent(ILogger logger, Type componentType, string path, string baseUri)
{
_navigatingToComponent(logger, componentType, path, baseUri, null);
}
internal static void NavigatingToExternalUri(ILogger logger, string externalUri, string path, string baseUri)
{
_navigatingToExternalUri(logger, externalUri, path, baseUri, null);
}
}
}
}