parent
6a24db5543
commit
2c3a44371a
|
|
@ -34,36 +34,32 @@ else
|
|||
</tbody>
|
||||
</table>
|
||||
<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
|
||||
</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 ▶
|
||||
</a>
|
||||
</p>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public DateTime StartDate { get; set; }
|
||||
[Parameter] public DateTime? StartDate { get; set; }
|
||||
|
||||
WeatherForecast[] forecasts;
|
||||
|
||||
public override Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
StartDate = DateTime.Now;
|
||||
return base.SetParametersAsync(parameters);
|
||||
}
|
||||
DateTime startDate;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
startDate = StartDate.GetValueOrDefault(DateTime.Now);
|
||||
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,
|
||||
// fake the DateFormatted values here. This would not apply in a real app.
|
||||
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)
|
||||
=> _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
|
||||
{
|
||||
public RouteEntry(RouteTemplate template, Type handler)
|
||||
public RouteEntry(RouteTemplate template, Type handler, string[] unusedRouteParameterNames)
|
||||
{
|
||||
Template = template;
|
||||
UnusedRouteParameterNames = unusedRouteParameterNames;
|
||||
Handler = handler;
|
||||
}
|
||||
|
||||
public RouteTemplate Template { get; }
|
||||
|
||||
public string[] UnusedRouteParameterNames { get; }
|
||||
|
||||
public Type Handler { get; }
|
||||
|
||||
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.Handler = Handler;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,19 +34,38 @@ namespace Microsoft.AspNetCore.Components
|
|||
|
||||
internal static RouteTable Create(IEnumerable<Type> componentTypes)
|
||||
{
|
||||
var routes = new List<RouteEntry>();
|
||||
foreach (var type in componentTypes)
|
||||
var templatesByHandler = new Dictionary<Type, string[]>();
|
||||
foreach (var componentType in componentTypes)
|
||||
{
|
||||
// We're deliberately using inherit = false here.
|
||||
//
|
||||
// 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.
|
||||
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 entry = new RouteEntry(template, type);
|
||||
var unusedRouteParameterNames = allRouteParameterNames
|
||||
.Except(GetParameterNames(parsedTemplate), StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
var entry = new RouteEntry(parsedTemplate, keyValuePair.Key, unusedRouteParameterNames);
|
||||
routes.Add(entry);
|
||||
}
|
||||
}
|
||||
|
|
@ -54,6 +73,14 @@ namespace Microsoft.AspNetCore.Components
|
|||
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>
|
||||
/// Route precedence algorithm.
|
||||
/// We collect all the routes and sort them from most specific to
|
||||
|
|
|
|||
|
|
@ -358,6 +358,24 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -268,8 +268,9 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
|
|||
{
|
||||
// Arrange
|
||||
var routeTable = new TestRouteTableBuilder()
|
||||
.AddRoute("/an/awesome/path")
|
||||
.AddRoute("/{some}/awesome/{route}/").Build();
|
||||
.AddRoute("/an/awesome/path", typeof(TestHandler1))
|
||||
.AddRoute("/{some}/awesome/{route}/", typeof(TestHandler2))
|
||||
.Build();
|
||||
var context = new RouteContext("/an/awesome/path");
|
||||
|
||||
// Act
|
||||
|
|
@ -346,9 +347,58 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
|
|||
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
|
||||
{
|
||||
IList<(string, Type)> _routeTemplates = new List<(string, Type)>();
|
||||
IList<(string Template, Type Handler)> _routeTemplates = new List<(string, Type)>();
|
||||
Type _handler = typeof(object);
|
||||
|
||||
public TestRouteTableBuilder AddRoute(string template, Type handler = null)
|
||||
|
|
@ -361,10 +411,10 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
|
|||
{
|
||||
try
|
||||
{
|
||||
return new RouteTable(_routeTemplates
|
||||
.Select(rt => new RouteEntry(TemplateParser.ParseTemplate(rt.Item1), rt.Item2))
|
||||
.OrderBy(id => id, RouteTableFactory.RoutePrecedence)
|
||||
.ToArray());
|
||||
var templatesByHandler = _routeTemplates
|
||||
.GroupBy(rt => rt.Handler)
|
||||
.ToDictionary(group => group.Key, group => group.Select(g => g.Template).ToArray());
|
||||
return RouteTableFactory.Create(templatesByHandler);
|
||||
}
|
||||
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");
|
||||
|
||||
// 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();
|
||||
Browser.Equal("Your full name is Abc .", () => app.FindElement(By.Id("test-info")).Text);
|
||||
AssertHighlightedLinks("With parameters");
|
||||
|
|
|
|||
|
|
@ -8,12 +8,4 @@
|
|||
[Parameter] public string FirstName { 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>
|
||||
</table>
|
||||
<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
|
||||
</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 ▶
|
||||
</a>
|
||||
</p>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public DateTime StartDate { get; set; }
|
||||
[Parameter] public DateTime? StartDate { get; set; }
|
||||
|
||||
WeatherForecast[] forecasts;
|
||||
|
||||
public override Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
StartDate = DateTime.Now;
|
||||
return base.SetParametersAsync(parameters);
|
||||
}
|
||||
DateTime startDate;
|
||||
|
||||
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