// 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.Linq; using System.Reflection; using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.Layouts; using Microsoft.AspNetCore.Blazor.RenderTree; namespace Microsoft.AspNetCore.Blazor.Browser.Routing { /// /// A component that displays whichever other component corresponds to the /// browser's changing navigation state. /// public class BrowserRouter : IComponent, IDisposable { static readonly char[] _queryOrHashStartChar = new[] { '?', '#' }; RenderHandle _renderHandle; string _baseUriPrefix; string _locationAbsolute; /// /// Gets or sets the assembly that should be searched, along with its referenced /// assemblies, for components matching the URI. /// public Assembly AppAssembly { get; set; } /// /// Gets or sets the namespace prefix that should be prepended when searching /// for matching components. /// public string PagesNamespace { get; set; } /// /// Gets or sets the component name that will be used if the URI ends with /// a slash. /// public string DefaultComponentName { get; set; } = "Index"; /// public void Init(RenderHandle renderHandle) { _renderHandle = renderHandle; UriHelper.EnableNavigationInteception(); UriHelper.OnLocationChanged += OnLocationChanged; _baseUriPrefix = UriHelper.GetBaseUriPrefix(); _locationAbsolute = UriHelper.GetAbsoluteUri(); } /// public void SetParameters(ParameterCollection parameters) { parameters.AssignToProperties(this); Refresh(); } /// public void Dispose() { UriHelper.OnLocationChanged -= OnLocationChanged; } protected virtual Type GetComponentTypeForPath(string locationPath) { if (AppAssembly == null) { throw new InvalidOperationException($"No value was specified for {nameof(AppAssembly)}."); } if (string.IsNullOrEmpty(PagesNamespace)) { throw new InvalidOperationException($"No value was specified for {nameof(PagesNamespace)}."); } locationPath = StringUntilAny(locationPath, _queryOrHashStartChar); var componentTypeName = $"{PagesNamespace}{locationPath.Replace('/', '.')}"; if (componentTypeName[componentTypeName.Length - 1] == '.') { componentTypeName += DefaultComponentName; } return FindComponentTypeInAssemblyOrReferences(AppAssembly, componentTypeName) ?? throw new InvalidOperationException($"{nameof(BrowserRouter)} cannot find any component type with name {componentTypeName}."); } private string StringUntilAny(string str, char[] chars) { var firstIndex = str.IndexOfAny(chars); return firstIndex < 0 ? str : str.Substring(0, firstIndex); } private Type FindComponentTypeInAssemblyOrReferences(Assembly assembly, string typeName) => assembly.GetType(typeName, throwOnError: false, ignoreCase: true) ?? assembly.GetReferencedAssemblies() .Select(Assembly.Load) .Select(referencedAssembly => FindComponentTypeInAssemblyOrReferences(referencedAssembly, typeName)) .FirstOrDefault(); protected virtual void Render(RenderTreeBuilder builder, Type matchedComponentType) { builder.OpenComponent(0, typeof(LayoutDisplay)); builder.AddAttribute(1, nameof(LayoutDisplay.Page), matchedComponentType); builder.CloseComponent(); } private void Refresh() { var locationPath = UriHelper.ToBaseRelativePath(_baseUriPrefix, _locationAbsolute); var matchedComponentType = GetComponentTypeForPath(locationPath); if (!typeof(IComponent).IsAssignableFrom(matchedComponentType)) { throw new InvalidOperationException($"The type {matchedComponentType.FullName} " + $"does not implement {typeof(IComponent).FullName}."); } _renderHandle.Render(builder => Render(builder, matchedComponentType)); } private void OnLocationChanged(object sender, string newAbsoluteUri) { _locationAbsolute = newAbsoluteUri; if (_renderHandle.IsInitialized) { Refresh(); } } } }