Use RouteValues and route parsing for passing parameters to views

Intercept NavigationManager and forward to IViewManager
This commit is contained in:
2025-08-02 12:59:45 +02:00
parent 56384b1b89
commit ad74955c18
11 changed files with 122 additions and 50 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>0.2.2</Version>
<Version>0.2.3</Version>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
+3
View File
@@ -6,6 +6,9 @@
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="9.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Routing" Version="2.3.0" />
<PackageVersion Include="Microsoft.AspNetCore.Routing.Abstractions" Version="2.3.0" />
<PackageVersion Include="Microsoft.AspNetCore.WebUtilities" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
</ItemGroup>
</Project>
+11 -6
View File
@@ -82,7 +82,7 @@ public class HomeViewModel : ViewModelBase<HomeViewModel, HomeView>
private void NavigateToProfile()
{
this.ViewManager.ShowView<UserProfileView, UserProfileViewModel>();
this.ViewManager.ShowView<UserProfileView>();
}
}
```
@@ -105,11 +105,14 @@ Inject `IViewManager` anywhere in your application to navigate:
[Inject]
private IViewManager ViewManager { get; set; }
// Navigate to a view with optional data context
ViewManager.ShowView<UserProfileView, UserProfileViewModel>(userData);
// Navigate to a view with optional parameters
ViewManager.ShowView<UserProfileView>([ {"user", userId} ]);
// Or navigate by type
ViewManager.ShowView(typeof(HomeView), someData);
ViewManager.ShowView(typeof(HomeView), [ {"user", userId} ]);
// Or navigate by path
ViewManager.ShowView("/user/profile", [ {"user", userId} ]);
```
## Core Concepts
@@ -131,8 +134,9 @@ Base class for all viewmodels that provides:
### IViewManager
Central service for managing view navigation:
- `ShowView<TView, TViewModel>(object? dataContext)` - Navigate with strong typing
- `ShowView(Type viewType, object? dataContext)` - Navigate by type
- `ShowView<TView>(RouteValues? routeValues)` - Navigate with strong typing
- `ShowView(Type viewType, RouteValues? routeValues)` - Navigate by type
- `ShowView(string path, RouteValues? routeValues)` - Navigate by path
- `ShowViewRequested` event - Subscribe to navigation events
### ViewContainer
@@ -141,6 +145,7 @@ Blazor component that hosts and renders the current view:
- Automatically updates when navigation occurs
- Supports dynamic component rendering
- Passes data context to views
- Hooks into `NavigationManager` to handle URL changes and forwards navigation requests to `IViewManager`
## Architecture
+6 -2
View File
@@ -4,7 +4,10 @@
@using TrailBlazr.Services
@code {
[Inject]
protected IViewManager? ViewManager { get; set; }
public required IViewManager ViewManager { get; init; }
[Inject]
public required NavigationManager NavigationManager { get; init; }
protected Type CurrentViewType { get; set; } = typeof(EmptyComponent);
@@ -17,12 +20,13 @@
throw new InvalidOperationException("ViewManager is not initialized.");
}
((ViewManager)this.ViewManager).ContainerInitialized(this.NavigationManager);
this.ViewManager.ShowViewRequested += this.UpdateCurrentView;
}
private void UpdateCurrentView(object? _, ViewRequest request)
{
this.CurrentParameters = new Dictionary<string, object>() { { "DataContext", request.DataContext ?? new object() } };
this.CurrentParameters = request.RouteValues.ToDictionary();
this.CurrentViewType = request.ViewType;
this.StateHasChanged();
}
+13 -4
View File
@@ -1,14 +1,23 @@
namespace TrailBlazr.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Routing.Template;
using System.Reflection;
public abstract class ViewRegistration(Type viewType, Type viewModelType)
namespace TrailBlazr.Models;
public abstract class ViewRegistration(Type viewType, Type viewModelType, IReadOnlyList<TemplateMatcher> routes)
{
public Type ViewType { get; } = viewType ?? throw new ArgumentNullException(nameof(viewType));
public Type ViewModelType { get; } = viewModelType ?? throw new ArgumentNullException(nameof(viewModelType));
public IReadOnlyList<TemplateMatcher> Routes { get; } = routes ?? throw new ArgumentNullException(nameof(routes));
}
public sealed class ViewRegistration<TView, TViewModel>() : ViewRegistration(typeof(TView), typeof(TViewModel))
public sealed class ViewRegistration<TView, TViewModel>()
: ViewRegistration(
typeof(TView),
typeof(TViewModel),
[.. typeof(TView).GetCustomAttributes<RouteAttribute>().Select(r => r.Template).Select(r => new TemplateMatcher(TemplateParser.Parse(r), []))])
where TView : Views.ViewBase<TView, TViewModel>
where TViewModel : ViewModels.ViewModelBase<TViewModel, TView>
{
public override string ToString() => $"({this.ViewType.Name} - {this.ViewModelType.Name})";
public override string ToString() => $"({this.ViewType.Name} - {this.ViewModelType.Name} ({string.Join(", ", this.Routes.Select(t => t.Template))}))";
}
+4 -2
View File
@@ -1,4 +1,6 @@
namespace TrailBlazr.Models;
public sealed record ViewRequest(Type ViewType, Type ViewModelType, object? DataContext)
using Microsoft.AspNetCore.Routing;
namespace TrailBlazr.Models;
public sealed record ViewRequest(Type ViewType, Type ViewModelType, RouteValueDictionary RouteValues)
{
}
+5 -2
View File
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Routing;
using TrailBlazr.Models;
namespace TrailBlazr.Services;
@@ -6,8 +7,10 @@ public interface IViewManager
{
event EventHandler<ViewRequest>? ShowViewRequested;
void ShowView<TView>(object? dataContext = null)
void ShowView<TView>(RouteValueDictionary? routeValues = default)
where TView : ComponentBase;
void ShowView(Type viewType, object? dataContext = null);
void ShowView(Type viewType, RouteValueDictionary? routeValues = default);
void ShowView(string path, RouteValueDictionary? routeValues = default);
}
+75 -9
View File
@@ -1,36 +1,102 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection;
using TrailBlazr.Models;
namespace TrailBlazr.Services;
public sealed class ViewManager(
IServiceProvider serviceProvider) : IViewManager
IServiceProvider serviceProvider) : IViewManager, IDisposable
{
private readonly IServiceProvider serviceProvider = serviceProvider;
private readonly IServiceProvider serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
private NavigationManager? navigationManager;
public event EventHandler<ViewRequest>? ShowViewRequested;
public void ShowView<TView>(object? dataContext = null)
internal void ContainerInitialized(NavigationManager navigationManager)
{
this.navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager));
this.navigationManager.LocationChanged += this.NavigationManager_LocationChanged;
}
public void ShowView<TView>(RouteValueDictionary? routeValues = default)
where TView : ComponentBase
{
var viewType = typeof(TView);
this.ShowViewInner(viewType, dataContext);
this.ShowViewByType(viewType, routeValues);
}
public void ShowView(Type viewType, object? dataContext = null)
public void ShowView(Type viewType, RouteValueDictionary? routeValues = default)
{
this.ShowViewInner(viewType, dataContext);
this.ShowViewByType(viewType, routeValues);
}
private void ShowViewInner(Type viewType, object? dataContext = null)
public void ShowView(string path, RouteValueDictionary? routeValues = default)
{
this.ShowViewByPath(path, routeValues);
}
private void ShowViewByType(Type viewType, RouteValueDictionary? routeValues)
{
var viewRegistrations = this.serviceProvider.GetServices<ViewRegistration>();
routeValues ??= [];
if (viewRegistrations.FirstOrDefault(r => r.ViewType == viewType) is not ViewRegistration registration)
{
throw new InvalidOperationException("View type not registered: " + viewType.FullName);
throw new InvalidOperationException($"View type not registered: {viewType.FullName}");
}
var viewRequest = new ViewRequest(registration.ViewType, registration.ViewModelType, dataContext);
this.ShowViewInner(registration.ViewType, registration.ViewModelType, routeValues);
}
private void ShowViewByPath(string path, RouteValueDictionary? routeValues)
{
var uriObj = path.StartsWith('/') ? new Uri(path, UriKind.Relative) : new Uri(path, UriKind.Absolute);
routeValues ??= [];
var query = uriObj.IsAbsoluteUri ? uriObj.Query : path[(path.IndexOf('?') + 1)..];
var finalPath = uriObj.IsAbsoluteUri ? uriObj.PathAndQuery.Split('?').FirstOrDefault() ?? string.Empty : path.Split('?').FirstOrDefault() ?? string.Empty;
foreach (var kvp in QueryHelpers.ParseNullableQuery(query) ?? [])
{
routeValues[kvp.Key] = kvp.Value.Count == 1 ? kvp.Value[0] : kvp.Value;
}
var viewRegistrations = this.serviceProvider.GetServices<ViewRegistration>();
routeValues ??= [];
if (viewRegistrations.FirstOrDefault(r => r.Routes.FirstOrDefault(t => {
var parsedRouteValues = new RouteValueDictionary();
if (t.TryMatch(finalPath, parsedRouteValues))
{
foreach(var kvp in parsedRouteValues)
{
routeValues[kvp.Key] = kvp.Value;
}
return true;
}
return false;
}) is not null) is not ViewRegistration registration)
{
throw new InvalidOperationException($"Path not found: {finalPath}");
}
this.ShowViewInner(registration.ViewType, registration.ViewModelType, routeValues);
}
private void ShowViewInner(Type viewType, Type viewModelType, RouteValueDictionary routeValues)
{
var viewRequest = new ViewRequest(viewType, viewModelType, routeValues);
this.ShowViewRequested?.Invoke(this, viewRequest);
}
public void Dispose()
{
this.navigationManager?.LocationChanged -= this.NavigationManager_LocationChanged;
}
private void NavigationManager_LocationChanged(object? sender, LocationChangedEventArgs e)
{
this.ShowViewByPath(e.Location, default);
}
}
+3
View File
@@ -12,6 +12,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components" />
<PackageReference Include="Microsoft.AspNetCore.Routing" />
<PackageReference Include="Microsoft.AspNetCore.Routing.Abstractions" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
</ItemGroup>
-10
View File
@@ -9,16 +9,6 @@ public abstract class ViewModelBase<TViewModel, TView> : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public object? DataContext
{
get => field;
set
{
field = value;
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.DataContext)));
}
}
protected TView? View
{
get => field;
+1 -14
View File
@@ -1,31 +1,18 @@
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;
using System.ComponentModel;
using TrailBlazr.ViewModels;
namespace TrailBlazr.Views;
public abstract class ViewBase<TView, TViewModel> : ComponentBase, INotifyPropertyChanged
public abstract class ViewBase<TView, TViewModel> : ComponentBase
where TView : ViewBase<TView, TViewModel>
where TViewModel : ViewModelBase<TViewModel, TView>
{
public readonly static TimeSpan InitializationTimeout = TimeSpan.FromSeconds(5);
public event PropertyChangedEventHandler? PropertyChanged;
[Inject]
public TViewModel ViewModel { get; set; } = default!;
[Inject]
protected ILogger<TView> Logger { get; set; } = default!;
[Parameter]
public object? DataContext
{
get => field;
set
{
field = value;
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.DataContext)));
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{