parent
6a24db5543
commit
2c3a44371a
|
|
@ -34,36 +34,32 @@ else
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p>
|
<p>
|
||||||
<a href="fetchdata/@StartDate.AddDays(-5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-left">
|
<a href="fetchdata/@startDate.AddDays(-5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-left">
|
||||||
◀ Previous
|
◀ Previous
|
||||||
</a>
|
</a>
|
||||||
<a href="fetchdata/@StartDate.AddDays(5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-right">
|
<a href="fetchdata/@startDate.AddDays(5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-right">
|
||||||
Next ▶
|
Next ▶
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter] public DateTime StartDate { get; set; }
|
[Parameter] public DateTime? StartDate { get; set; }
|
||||||
|
|
||||||
WeatherForecast[] forecasts;
|
WeatherForecast[] forecasts;
|
||||||
|
DateTime startDate;
|
||||||
public override Task SetParametersAsync(ParameterCollection parameters)
|
|
||||||
{
|
|
||||||
StartDate = DateTime.Now;
|
|
||||||
return base.SetParametersAsync(parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
|
startDate = StartDate.GetValueOrDefault(DateTime.Now);
|
||||||
forecasts = await Http.GetJsonAsync<WeatherForecast[]>(
|
forecasts = await Http.GetJsonAsync<WeatherForecast[]>(
|
||||||
$"sample-data/weather.json?date={StartDate.ToString("yyyy-MM-dd")}");
|
$"sample-data/weather.json?date={startDate.ToString("yyyy-MM-dd")}");
|
||||||
|
|
||||||
// Because StandaloneApp doesn't really have a server endpoint to get dynamic data from,
|
// Because StandaloneApp doesn't really have a server endpoint to get dynamic data from,
|
||||||
// fake the DateFormatted values here. This would not apply in a real app.
|
// fake the DateFormatted values here. This would not apply in a real app.
|
||||||
for (var i = 0; i < forecasts.Length; i++)
|
for (var i = 0; i < forecasts.Length; i++)
|
||||||
{
|
{
|
||||||
forecasts[i].DateFormatted = StartDate.AddDays(i).ToShortDateString();
|
forecasts[i].DateFormatted = startDate.AddDays(i).ToShortDateString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,16 @@ namespace Microsoft.AspNetCore.Components.Reflection
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetValue(object target, object value)
|
public void SetValue(object target, object value)
|
||||||
=> _setterDelegate((TTarget)target, (TValue)value);
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
_setterDelegate((TTarget)target, default);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_setterDelegate((TTarget)target, (TValue)value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,17 @@ namespace Microsoft.AspNetCore.Components.Routing
|
||||||
{
|
{
|
||||||
internal class RouteEntry
|
internal class RouteEntry
|
||||||
{
|
{
|
||||||
public RouteEntry(RouteTemplate template, Type handler)
|
public RouteEntry(RouteTemplate template, Type handler, string[] unusedRouteParameterNames)
|
||||||
{
|
{
|
||||||
Template = template;
|
Template = template;
|
||||||
|
UnusedRouteParameterNames = unusedRouteParameterNames;
|
||||||
Handler = handler;
|
Handler = handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RouteTemplate Template { get; }
|
public RouteTemplate Template { get; }
|
||||||
|
|
||||||
|
public string[] UnusedRouteParameterNames { get; }
|
||||||
|
|
||||||
public Type Handler { get; }
|
public Type Handler { get; }
|
||||||
|
|
||||||
internal void Match(RouteContext context)
|
internal void Match(RouteContext context)
|
||||||
|
|
@ -45,6 +48,18 @@ namespace Microsoft.AspNetCore.Components.Routing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In addition to extracting parameter values from the URL, each route entry
|
||||||
|
// also knows which other parameters should be supplied with null values. These
|
||||||
|
// are parameters supplied by other route entries matching the same handler.
|
||||||
|
if (UnusedRouteParameterNames.Length > 0)
|
||||||
|
{
|
||||||
|
parameters ??= new Dictionary<string, object>(StringComparer.Ordinal);
|
||||||
|
for (var i = 0; i < UnusedRouteParameterNames.Length; i++)
|
||||||
|
{
|
||||||
|
parameters[UnusedRouteParameterNames[i]] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
context.Parameters = parameters;
|
context.Parameters = parameters;
|
||||||
context.Handler = Handler;
|
context.Handler = Handler;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,19 +34,38 @@ namespace Microsoft.AspNetCore.Components
|
||||||
|
|
||||||
internal static RouteTable Create(IEnumerable<Type> componentTypes)
|
internal static RouteTable Create(IEnumerable<Type> componentTypes)
|
||||||
{
|
{
|
||||||
var routes = new List<RouteEntry>();
|
var templatesByHandler = new Dictionary<Type, string[]>();
|
||||||
foreach (var type in componentTypes)
|
foreach (var componentType in componentTypes)
|
||||||
{
|
{
|
||||||
// We're deliberately using inherit = false here.
|
// We're deliberately using inherit = false here.
|
||||||
//
|
//
|
||||||
// RouteAttribute is defined as non-inherited, because inheriting a route attribute always causes an
|
// RouteAttribute is defined as non-inherited, because inheriting a route attribute always causes an
|
||||||
// ambiguity. You end up with two components (base class and derived class) with the same route.
|
// ambiguity. You end up with two components (base class and derived class) with the same route.
|
||||||
var routeAttributes = type.GetCustomAttributes<RouteAttribute>(inherit: false);
|
var routeAttributes = componentType.GetCustomAttributes<RouteAttribute>(inherit: false);
|
||||||
|
|
||||||
foreach (var routeAttribute in routeAttributes)
|
var templates = routeAttributes.Select(t => t.Template).ToArray();
|
||||||
|
templatesByHandler.Add(componentType, templates);
|
||||||
|
}
|
||||||
|
return Create(templatesByHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static RouteTable Create(Dictionary<Type, string[]> templatesByHandler)
|
||||||
|
{
|
||||||
|
var routes = new List<RouteEntry>();
|
||||||
|
foreach (var keyValuePair in templatesByHandler)
|
||||||
|
{
|
||||||
|
var parsedTemplates = keyValuePair.Value.Select(v => TemplateParser.ParseTemplate(v)).ToArray();
|
||||||
|
var allRouteParameterNames = parsedTemplates
|
||||||
|
.SelectMany(GetParameterNames)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var parsedTemplate in parsedTemplates)
|
||||||
{
|
{
|
||||||
var template = TemplateParser.ParseTemplate(routeAttribute.Template);
|
var unusedRouteParameterNames = allRouteParameterNames
|
||||||
var entry = new RouteEntry(template, type);
|
.Except(GetParameterNames(parsedTemplate), StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
var entry = new RouteEntry(parsedTemplate, keyValuePair.Key, unusedRouteParameterNames);
|
||||||
routes.Add(entry);
|
routes.Add(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -54,6 +73,14 @@ namespace Microsoft.AspNetCore.Components
|
||||||
return new RouteTable(routes.OrderBy(id => id, RoutePrecedence).ToArray());
|
return new RouteTable(routes.OrderBy(id => id, RoutePrecedence).ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string[] GetParameterNames(RouteTemplate routeTemplate)
|
||||||
|
{
|
||||||
|
return routeTemplate.Segments
|
||||||
|
.Where(s => s.IsParameter)
|
||||||
|
.Select(s => s.Value)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Route precedence algorithm.
|
/// Route precedence algorithm.
|
||||||
/// We collect all the routes and sort them from most specific to
|
/// We collect all the routes and sort them from most specific to
|
||||||
|
|
|
||||||
|
|
@ -358,6 +358,24 @@ namespace Microsoft.AspNetCore.Components.Test
|
||||||
ex.Message);
|
ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SupplyingNullWritesDefaultForType()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var parameterCollection = new ParameterCollectionBuilder
|
||||||
|
{
|
||||||
|
{ nameof(HasInstanceProperties.IntProp), null },
|
||||||
|
{ nameof(HasInstanceProperties.StringProp), null },
|
||||||
|
}.Build();
|
||||||
|
var target = new HasInstanceProperties { IntProp = 123, StringProp = "Hello" };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
parameterCollection.SetParameterProperties(target);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(0, target.IntProp);
|
||||||
|
Assert.Null(target.StringProp);
|
||||||
|
}
|
||||||
|
|
||||||
class HasInstanceProperties
|
class HasInstanceProperties
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -268,8 +268,9 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var routeTable = new TestRouteTableBuilder()
|
var routeTable = new TestRouteTableBuilder()
|
||||||
.AddRoute("/an/awesome/path")
|
.AddRoute("/an/awesome/path", typeof(TestHandler1))
|
||||||
.AddRoute("/{some}/awesome/{route}/").Build();
|
.AddRoute("/{some}/awesome/{route}/", typeof(TestHandler2))
|
||||||
|
.Build();
|
||||||
var context = new RouteContext("/an/awesome/path");
|
var context = new RouteContext("/an/awesome/path");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -346,9 +347,58 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
|
||||||
Assert.Equal(expectedMessage, exception.Message);
|
Assert.Equal(expectedMessage, exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SuppliesNullForUnusedHandlerParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var routeTable = new TestRouteTableBuilder()
|
||||||
|
.AddRoute("/", typeof(TestHandler1))
|
||||||
|
.AddRoute("/products/{param1:int}", typeof(TestHandler1))
|
||||||
|
.AddRoute("/products/{param2}/{PaRam1}", typeof(TestHandler1))
|
||||||
|
.AddRoute("/{unrelated}", typeof(TestHandler2))
|
||||||
|
.Build();
|
||||||
|
var context = new RouteContext("/products/456");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
routeTable.Route(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(routeTable.Routes,
|
||||||
|
route =>
|
||||||
|
{
|
||||||
|
Assert.Same(typeof(TestHandler1), route.Handler);
|
||||||
|
Assert.Equal("/", route.Template.TemplateText);
|
||||||
|
Assert.Equal(new[] { "param1", "param2" }, route.UnusedRouteParameterNames);
|
||||||
|
},
|
||||||
|
route =>
|
||||||
|
{
|
||||||
|
Assert.Same(typeof(TestHandler2), route.Handler);
|
||||||
|
Assert.Equal("{unrelated}", route.Template.TemplateText);
|
||||||
|
Assert.Equal(Array.Empty<string>(), route.UnusedRouteParameterNames);
|
||||||
|
},
|
||||||
|
route =>
|
||||||
|
{
|
||||||
|
Assert.Same(typeof(TestHandler1), route.Handler);
|
||||||
|
Assert.Equal("products/{param1:int}", route.Template.TemplateText);
|
||||||
|
Assert.Equal(new[] { "param2" }, route.UnusedRouteParameterNames);
|
||||||
|
},
|
||||||
|
route =>
|
||||||
|
{
|
||||||
|
Assert.Same(typeof(TestHandler1), route.Handler);
|
||||||
|
Assert.Equal("products/{param2}/{PaRam1}", route.Template.TemplateText);
|
||||||
|
Assert.Equal(Array.Empty<string>(), route.UnusedRouteParameterNames);
|
||||||
|
});
|
||||||
|
Assert.Same(typeof(TestHandler1), context.Handler);
|
||||||
|
Assert.Equal(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "param1", 456 },
|
||||||
|
{ "param2", null },
|
||||||
|
}, context.Parameters);
|
||||||
|
}
|
||||||
|
|
||||||
private class TestRouteTableBuilder
|
private class TestRouteTableBuilder
|
||||||
{
|
{
|
||||||
IList<(string, Type)> _routeTemplates = new List<(string, Type)>();
|
IList<(string Template, Type Handler)> _routeTemplates = new List<(string, Type)>();
|
||||||
Type _handler = typeof(object);
|
Type _handler = typeof(object);
|
||||||
|
|
||||||
public TestRouteTableBuilder AddRoute(string template, Type handler = null)
|
public TestRouteTableBuilder AddRoute(string template, Type handler = null)
|
||||||
|
|
@ -361,10 +411,10 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return new RouteTable(_routeTemplates
|
var templatesByHandler = _routeTemplates
|
||||||
.Select(rt => new RouteEntry(TemplateParser.ParseTemplate(rt.Item1), rt.Item2))
|
.GroupBy(rt => rt.Handler)
|
||||||
.OrderBy(id => id, RouteTableFactory.RoutePrecedence)
|
.ToDictionary(group => group.Key, group => group.Select(g => g.Template).ToArray());
|
||||||
.ToArray());
|
return RouteTableFactory.Create(templatesByHandler);
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException ex) when (ex.InnerException is InvalidOperationException)
|
catch (InvalidOperationException ex) when (ex.InnerException is InvalidOperationException)
|
||||||
{
|
{
|
||||||
|
|
@ -373,5 +423,8 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TestHandler1 { }
|
||||||
|
class TestHandler2 { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -226,10 +226,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
AssertHighlightedLinks("With parameters", "With more parameters");
|
AssertHighlightedLinks("With parameters", "With more parameters");
|
||||||
|
|
||||||
// Can remove parameters while remaining on same page
|
// Can remove parameters while remaining on same page
|
||||||
// WARNING: This only works because the WithParameters component overrides SetParametersAsync
|
|
||||||
// and explicitly resets its parameters to default when each new set of parameters arrives.
|
|
||||||
// Without that, the page would retain the old value.
|
|
||||||
// See https://github.com/aspnet/AspNetCore/issues/6864 where we reverted the logic to auto-reset.
|
|
||||||
app.FindElement(By.LinkText("With parameters")).Click();
|
app.FindElement(By.LinkText("With parameters")).Click();
|
||||||
Browser.Equal("Your full name is Abc .", () => app.FindElement(By.Id("test-info")).Text);
|
Browser.Equal("Your full name is Abc .", () => app.FindElement(By.Id("test-info")).Text);
|
||||||
AssertHighlightedLinks("With parameters");
|
AssertHighlightedLinks("With parameters");
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,4 @@
|
||||||
[Parameter] public string FirstName { get; set; }
|
[Parameter] public string FirstName { get; set; }
|
||||||
|
|
||||||
[Parameter] public string LastName { get ; set; }
|
[Parameter] public string LastName { get ; set; }
|
||||||
|
|
||||||
public override Task SetParametersAsync(ParameterCollection parameters)
|
|
||||||
{
|
|
||||||
// Manually reset parameters to defaults so we don't retain any from an earlier URL
|
|
||||||
FirstName = default;
|
|
||||||
LastName = default;
|
|
||||||
return base.SetParametersAsync(parameters);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,28 +34,24 @@ else
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p>
|
<p>
|
||||||
<a href="fetchdata/@StartDate.AddDays(-5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-left">
|
<a href="fetchdata/@startDate.AddDays(-5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-left">
|
||||||
◀ Previous
|
◀ Previous
|
||||||
</a>
|
</a>
|
||||||
<a href="fetchdata/@StartDate.AddDays(5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-right">
|
<a href="fetchdata/@startDate.AddDays(5).ToString("yyyy-MM-dd")" class="btn btn-secondary float-right">
|
||||||
Next ▶
|
Next ▶
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter] public DateTime StartDate { get; set; }
|
[Parameter] public DateTime? StartDate { get; set; }
|
||||||
|
|
||||||
WeatherForecast[] forecasts;
|
WeatherForecast[] forecasts;
|
||||||
|
DateTime startDate;
|
||||||
public override Task SetParametersAsync(ParameterCollection parameters)
|
|
||||||
{
|
|
||||||
StartDate = DateTime.Now;
|
|
||||||
return base.SetParametersAsync(parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
forecasts = await ForecastService.GetForecastAsync(StartDate);
|
startDate = StartDate.GetValueOrDefault(DateTime.Now);
|
||||||
|
forecasts = await ForecastService.GetForecastAsync(startDate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue