Skip to content
This repository has been archived by the owner on Feb 25, 2021. It is now read-only.

Commit

Permalink
[Fixes #220] Support @page with custom route template on components
Browse files Browse the repository at this point in the history
* Updates the router component to scan for components within assemblies.
* Parses the templates on `[Route]` in component instances and builds a
  route table that maps paths to components.
* Uses the route table to map paths to components.
  • Loading branch information
javiercn committed Mar 17, 2018
1 parent 53cf7c6 commit 841c777
Show file tree
Hide file tree
Showing 22 changed files with 951 additions and 65 deletions.
4 changes: 1 addition & 3 deletions samples/StandaloneApp/App.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,4 @@
Configuring this stuff here is temporary. Later we'll move the app config
into Program.cs, and it won't be necessary to specify AppAssembly.
-->
<Router
AppAssembly=typeof(StandaloneApp.Program).Assembly
PagesNamespace="StandaloneApp.Pages" />
<Router AppAssembly=typeof(StandaloneApp.Program).Assembly />
3 changes: 2 additions & 1 deletion samples/StandaloneApp/Pages/Counter.cshtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<h1>Counter</h1>
@page "/counter"
<h1>Counter</h1>

<p>Current count: @currentCount</p>

Expand Down
3 changes: 2 additions & 1 deletion samples/StandaloneApp/Pages/FetchData.cshtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@inject HttpClient Http
@page "/fetchdata"
@inject HttpClient Http

<h1>Weather forecast</h1>

Expand Down
3 changes: 2 additions & 1 deletion samples/StandaloneApp/Pages/Index.cshtml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<h1>Hello, world!</h1>
@page "/"
<h1>Hello, world!</h1>

Welcome to your new app.
73 changes: 73 additions & 0 deletions src/Microsoft.AspNetCore.Blazor/Components/ComponentResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// 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.Linq;
using System.Reflection;

namespace Microsoft.AspNetCore.Blazor.Components
{
/// <summary>
/// Resolves components for an application.
/// </summary>
internal class ComponentResolver
{
/// <summary>
/// Lists all the types
/// </summary>
/// <param name="appAssembly"></param>
/// <returns></returns>
public static IEnumerable<Type> ResolveComponents(Assembly appAssembly)
{
var blazorAssembly = typeof(IComponent).Assembly;

return EnumerateAssemblies(appAssembly.GetName(), blazorAssembly, new HashSet<Assembly>(new AssemblyComparer()))
.SelectMany(a => a.ExportedTypes)
.Where(t => typeof(IComponent).IsAssignableFrom(t));
}

private static IEnumerable<Assembly> EnumerateAssemblies(
AssemblyName assemblyName,
Assembly blazorAssembly,
HashSet<Assembly> visited)
{
var assembly = Assembly.Load(assemblyName);
if (visited.Contains(assembly))
{
// Avoid traversing visited assemblies.
yield break;
}
visited.Add(assembly);
var references = assembly.GetReferencedAssemblies();
if (!references.Any(r => string.Equals(r.FullName, blazorAssembly.FullName, StringComparison.Ordinal)))
{
// Avoid traversing references that don't point to blazor (like netstandard2.0)
yield break;
}
else
{
yield return assembly;

// Look at the list of transitive dependencies for more components.
foreach (var reference in references.SelectMany(r => EnumerateAssemblies(r, blazorAssembly, visited)))
{
yield return reference;
}
}
}

private class AssemblyComparer : IEqualityComparer<Assembly>
{
public bool Equals(Assembly x, Assembly y)
{
return string.Equals(x?.FullName, y?.FullName, StringComparison.Ordinal);
}

public int GetHashCode(Assembly obj)
{
return obj.FullName.GetHashCode();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ private static void AssignToProperty(object target, Parameter parameter)

private static PropertyInfo GetPropertyInfo(Type targetType, string propertyName)
{
var property = targetType.GetProperty(propertyName);
var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;
var property = targetType.GetProperty(propertyName, flags);
if (property == null)
{
throw new InvalidOperationException(
Expand Down
20 changes: 18 additions & 2 deletions src/Microsoft.AspNetCore.Blazor/Layouts/LayoutDisplay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// 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.Reflection;
using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.RenderTree;
Expand All @@ -15,13 +16,18 @@ namespace Microsoft.AspNetCore.Blazor.Layouts
public class LayoutDisplay : IComponent
{
private RenderHandle _renderHandle;

/// <summary>
/// Gets or sets the type of the page component to display.
/// The type must implement <see cref="IComponent"/>.
/// </summary>
public Type Page { get; set; }

/// <summary>
/// Gets or sets the parameters to pass to the page.
/// </summary>
public IDictionary<string, string> PageParameters { get; set; }

/// <inheritdoc />
public void Init(RenderHandle renderHandle)
{
Expand All @@ -47,7 +53,7 @@ private void Render()
{
fragment = RenderComponentWithBody(layoutType, fragment);
}

_renderHandle.Render(fragment);
}

Expand All @@ -58,6 +64,16 @@ private RenderFragment RenderComponentWithBody(Type componentType, RenderFragmen
{
builder.AddAttribute(1, nameof(ILayoutComponent.Body), bodyParam);
}
else
{
if (PageParameters != null)
{
foreach (var kvp in PageParameters)
{
builder.AddAttribute(1, kvp.Key, kvp.Value);
}
}
}
builder.CloseComponent();
};

Expand Down
26 changes: 26 additions & 0 deletions src/Microsoft.AspNetCore.Blazor/Routing/RouteContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// 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;

namespace Microsoft.AspNetCore.Blazor.Routing
{
internal class RouteContext
{
private static char[] Separator = new[] { '/' };

public RouteContext(string path)
{
// This is a simplification. We are assuming there are no paths like /a//b/. A proper routing
// implementation would be more sophisticated.
Segments = path.Trim('/').Split(Separator, StringSplitOptions.RemoveEmptyEntries);
}

public string[] Segments { get; }

public Type Handler { get; set; }

public IDictionary<string, string> Parameters { get; set; }
}
}
61 changes: 61 additions & 0 deletions src/Microsoft.AspNetCore.Blazor/Routing/RouteEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// 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;

namespace Microsoft.AspNetCore.Blazor.Routing
{
internal class RouteEntry
{
public RouteEntry(RouteTemplate template, Type handler)
{
Template = template;
Handler = handler;
}

public RouteTemplate Template { get; }

public Type Handler { get; }

internal void Match(RouteContext context)
{
if (Template.Segments.Length != context.Segments.Length)
{
return;
}

// Parameters will be lazily initialized.
IDictionary<string, string> parameters = null;
for (int i = 0; i < Template.Segments.Length; i++)
{
var segment = Template.Segments[i];
var pathSegment = context.Segments[i];
if (!segment.Match(pathSegment))
{
return;
}
else
{
if (segment.IsParameter)
{
GetParameters()[segment.Value] = pathSegment;
}
}
}

context.Parameters = parameters;
context.Handler = Handler;

IDictionary<string, string> GetParameters()
{
if (parameters == null)
{
parameters = new Dictionary<string, string>();
}

return parameters;
}
}
}
}
130 changes: 130 additions & 0 deletions src/Microsoft.AspNetCore.Blazor/Routing/RouteTable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// 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.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Blazor.Components;

namespace Microsoft.AspNetCore.Blazor.Routing
{
internal class RouteTable
{
public RouteTable(RouteEntry[] routes)
{
Routes = routes;
}

public RouteEntry[] Routes { get; set; }

public static RouteTable Create(IEnumerable<Type> types)
{
var routes = new List<RouteEntry>();
foreach (var type in types)
{
var routeAttributes = type.GetCustomAttributes<RouteAttribute>(); // Inherit: true?
foreach (var routeAttribute in routeAttributes)
{
var template = TemplateParser.ParseTemplate(routeAttribute.Template);
var entry = new RouteEntry(template, type);
routes.Add(entry);
}
}

return new RouteTable(routes.OrderBy(id => id, RoutePrecedence).ToArray());
}

public static IComparer<RouteEntry> RoutePrecedence { get; } = Comparer<RouteEntry>.Create(RouteComparison);

/// <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.
/// * 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}
///
/// Routes can be ambigous 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
///
/// 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 insenitive) we choose the lexical order
/// If we get to the end of the comparison routing we've detected an ambigous pair of routes.
internal static int RouteComparison(RouteEntry x, RouteEntry y)
{
var xTemplate = x.Template;
var yTemplate = y.Template;
if (xTemplate.Segments.Length != y.Template.Segments.Length)
{
return xTemplate.Segments.Length < y.Template.Segments.Length ? -1 : 1;
}
else
{
for (int i = 0; i < xTemplate.Segments.Length; i++)
{
var xSegment = xTemplate.Segments[i];
var ySegment = yTemplate.Segments[i];
if (!xSegment.IsParameter && ySegment.IsParameter)
{
return -1;
}
if (xSegment.IsParameter && !ySegment.IsParameter)
{
return 1;
}
}

for (int i = 0; i < xTemplate.Segments.Length; i++)
{
var xSegment = xTemplate.Segments[i];
var ySegment = yTemplate.Segments[i];
if (!xSegment.IsParameter && ySegment.IsParameter)
{
return -1;
}
if (xSegment.IsParameter && !ySegment.IsParameter)
{
return 1;
}

if (!xSegment.IsParameter)
{
var comparison = string.Compare(xSegment.Value, ySegment.Value, StringComparison.OrdinalIgnoreCase);
if (comparison != 0)
{
return comparison;
}
}
}

throw new InvalidOperationException($@"The following routes are ambiguous:
'{x.Template.TemplateText}' in '{x.Handler.FullName}'
'{y.Template.TemplateText}' in '{y.Handler.FullName}'
");
}
}

internal void Route(RouteContext routeContext)
{
foreach (var route in Routes)
{
route.Match(routeContext);
if (routeContext.Handler != null)
{
return;
}
}
}
}
}
Loading

0 comments on commit 841c777

Please sign in to comment.