-
-
Notifications
You must be signed in to change notification settings - Fork 119
feat: Navigation will set parameters #1584
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,113 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| namespace Bunit.TestDoubles; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Globalization; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Reflection; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| internal sealed class ComponentRouteParameterService | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly BunitContext bunitContext; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Initializes a new instance of the <see cref="ComponentRouteParameterService"/> class. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public ComponentRouteParameterService(BunitContext bunitContext) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.bunitContext = bunitContext; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Triggers the components to update their parameters based on the route parameters. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void UpdateComponentsWithRouteParameters(Uri uri) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ArgumentNullException.ThrowIfNull(uri); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var relativeUri = uri.PathAndQuery; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| foreach (var renderedComponent in bunitContext.ReturnedRenderedComponents) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var instance = renderedComponent.Instance; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var routeAttributes = GetRouteAttributesFromComponent(instance); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (routeAttributes.Length == 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| foreach (var template in routeAttributes.Select(r => r.Template)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var parameters = GetParametersFromTemplateAndUri(template, relativeUri, instance); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (parameters.Count > 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bunitContext.Renderer.SetDirectParametersAsync(renderedComponent, ParameterView.FromDictionary(parameters)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static RouteAttribute[] GetRouteAttributesFromComponent(IComponent instance) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| instance.GetType() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .GetCustomAttributes(typeof(RouteAttribute), true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .Cast<RouteAttribute>() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .ToArray(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static Dictionary<string, object?> GetParametersFromTemplateAndUri(string template, string relativeUri, IComponent instance) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var templateSegments = template.Trim('/').Split("/"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var uriSegments = relativeUri.Trim('/').Split("/"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (templateSegments.Length > uriSegments.Length) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+59
to
+62
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var parameters = new Dictionary<string, object?>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (var i = 0; i < templateSegments.Length; i++) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var templateSegment = templateSegments[i]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (templateSegment.StartsWith('{') && templateSegment.EndsWith('}')) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var parameterName = GetParameterName(templateSegment); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var property = GetParameterProperty(instance, parameterName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (property is null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var isCatchAllParameter = templateSegment[1] == '*'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parameters[property.Name] = isCatchAllParameter | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? string.Join("/", uriSegments.Skip(i)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : GetValue(uriSegments[i], property); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else if (templateSegment != uriSegments[i]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return parameters; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static string GetParameterName(string templateSegment) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| templateSegment | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .Trim('{', '}', '*') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .Replace("?", string.Empty, StringComparison.OrdinalIgnoreCase) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .Split(':')[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static PropertyInfo? GetParameterProperty(object instance, string propertyName) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var propertyInfos = instance.GetType() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .GetProperties(BindingFlags.Public | BindingFlags.Instance); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Array.Find(propertyInfos, prop => prop.GetCustomAttributes(typeof(ParameterAttribute), true).Length > 0 && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| string.Equals(prop.Name, propertyName, StringComparison.OrdinalIgnoreCase)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static object GetValue(string value, PropertyInfo property) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var underlyingType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Convert.ChangeType(value, underlyingType, CultureInfo.InvariantCulture); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+108
to
+112
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static object GetValue(string value, PropertyInfo property) | |
| { | |
| var underlyingType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; | |
| return Convert.ChangeType(value, underlyingType, CultureInfo.InvariantCulture); | |
| } | |
| private static object? GetValue(string value, PropertyInfo property) | |
| { | |
| var underlyingType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; | |
| try | |
| { | |
| return Convert.ChangeType(value, underlyingType, CultureInfo.InvariantCulture); | |
| } | |
| catch (FormatException) | |
| { | |
| return GetDefaultValue(underlyingType); | |
| } | |
| catch (InvalidCastException) | |
| { | |
| return GetDefaultValue(underlyingType); | |
| } | |
| catch (OverflowException) | |
| { | |
| return GetDefaultValue(underlyingType); | |
| } | |
| } | |
| private static object? GetDefaultValue(Type type) | |
| { | |
| // Return null for reference types and Nullable<T>, otherwise default(T) | |
| if (!type.IsValueType || Nullable.GetUnderlyingType(type) != null) | |
| return null; | |
| return Activator.CreateInstance(type); | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| namespace Bunit.TestDoubles; | ||
|
|
||
| public partial class RouterTests | ||
| { | ||
| [Route("/page/{count:int}/{name}")] | ||
| private sealed class ComponentWithPageAttribute : ComponentBase | ||
| { | ||
| [Parameter] public int Count { get; set; } | ||
| [Parameter] public string Name { get; set; } | ||
| protected override void BuildRenderTree(RenderTreeBuilder builder) | ||
| { | ||
| builder.OpenElement(0, "p"); | ||
| builder.AddContent(1, Count); | ||
| builder.AddContent(2, " / "); | ||
| builder.AddContent(3, Name); | ||
| builder.CloseElement(); | ||
| } | ||
| } | ||
|
|
||
| [Route("/page")] | ||
| [Route("/page/{count:int}")] | ||
| private sealed class ComponentWithMultiplePageAttributes : ComponentBase | ||
| { | ||
| [Parameter] public int Count { get; set; } | ||
| protected override void BuildRenderTree(RenderTreeBuilder builder) | ||
| { | ||
| builder.OpenElement(0, "p"); | ||
| builder.AddContent(1, Count); | ||
| builder.CloseElement(); | ||
| } | ||
| } | ||
|
|
||
| [Route("/page/{count:int}")] | ||
| private sealed class ComponentWithOtherParameters : ComponentBase | ||
| { | ||
| [Parameter] public int Count { get; set; } | ||
| [Parameter] public int OtherNumber { get; set; } | ||
|
|
||
| protected override void BuildRenderTree(RenderTreeBuilder builder) | ||
| { | ||
| builder.OpenElement(0, "p"); | ||
| builder.AddContent(1, Count); | ||
| builder.AddContent(2, "/"); | ||
| builder.AddContent(3, OtherNumber); | ||
| builder.CloseElement(); | ||
| } | ||
| } | ||
|
|
||
| [Route("/page/{*pageRoute}")] | ||
| private sealed class ComponentWithCatchAllRoute : ComponentBase | ||
| { | ||
| [Parameter] public string PageRoute { get; set; } | ||
|
|
||
| protected override void BuildRenderTree(RenderTreeBuilder builder) | ||
| { | ||
| builder.OpenElement(0, "p"); | ||
| builder.AddContent(1, PageRoute); | ||
| builder.CloseElement(); | ||
| } | ||
| } | ||
|
|
||
| [Route("/page/{count:int}")] | ||
| private sealed class ComponentWithCustomOnParametersSetAsyncsCall : ComponentBase | ||
| { | ||
| [Parameter] public int Count { get; set; } | ||
| [Parameter] public int IncrementOnParametersSet { get; set; } | ||
|
|
||
| protected override void OnParametersSet() | ||
| { | ||
| Count += IncrementOnParametersSet; | ||
| } | ||
|
|
||
| protected override void BuildRenderTree(RenderTreeBuilder builder) | ||
| { | ||
| builder.OpenElement(0, "p"); | ||
| builder.AddContent(1, Count); | ||
| builder.CloseElement(); | ||
| } | ||
| } | ||
|
|
||
| [Route("/page/{count?:int}")] | ||
| private sealed class ComponentWithOptionalParameter : ComponentBase | ||
| { | ||
| [Parameter] public int? Count { get; set; } | ||
|
|
||
| protected override void BuildRenderTree(RenderTreeBuilder builder) | ||
| { | ||
| builder.OpenElement(0, "p"); | ||
| builder.AddContent(1, Count); | ||
| builder.CloseElement(); | ||
| } | ||
| } | ||
|
|
||
| [Route("/page/{count:int}")] | ||
| private sealed class ComponentThatNavigatesToSelfOnButtonClick : ComponentBase | ||
| { | ||
| [Parameter] public int Count { get; set; } | ||
|
|
||
| [Inject] private NavigationManager NavigationManager { get; set; } | ||
|
|
||
| protected override void BuildRenderTree(RenderTreeBuilder builder) | ||
| { | ||
| builder.OpenElement(0, "button"); | ||
| builder.AddAttribute(1, "onclick", EventCallback.Factory.Create(this, () => NavigationManager.NavigateTo($"/page/{Count + 1}"))); | ||
| builder.AddContent(2, "Increment"); | ||
| builder.CloseElement(); | ||
| builder.OpenElement(3, "p"); | ||
| builder.AddContent(4, Count); | ||
| builder.CloseElement(); | ||
| } | ||
| } | ||
|
|
||
| [Route("/page/{count:int}")] | ||
| private sealed class ComponentThatNavigatesToSelfOnButtonClickIntercepted : ComponentBase | ||
| { | ||
| [Parameter] public int Count { get; set; } | ||
|
|
||
| [Inject] private NavigationManager NavigationManager { get; set; } | ||
|
|
||
| protected override void BuildRenderTree(RenderTreeBuilder builder) | ||
| { | ||
| builder.OpenElement(0, "button"); | ||
| builder.AddAttribute(1, "onclick", EventCallback.Factory.Create(this, () => NavigationManager.NavigateTo($"/page/{Count + 1}"))); | ||
| builder.AddContent(2, "Increment"); | ||
| builder.CloseElement(); | ||
| builder.OpenElement(3, "p"); | ||
| builder.AddContent(4, Count); | ||
| builder.CloseElement(); | ||
| builder.OpenComponent<NavigationLock>(5); | ||
| builder.AddAttribute(6, "OnBeforeInternalNavigation", | ||
| EventCallback.Factory.Create<LocationChangingContext>(this, | ||
| InterceptNavigation | ||
| )); | ||
| builder.CloseComponent(); | ||
| } | ||
|
|
||
| private static void InterceptNavigation(LocationChangingContext context) | ||
| { | ||
| context.PreventNavigation(); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TODO comment should be replaced with proper documentation describing the purpose and usage of the ReturnedRenderedComponents property.