mirror of
https://github.com/AlexMacocian/TrailBlazr.git
synced 2026-07-15 15:20:00 +00:00
Use RouteValues and route parsing for passing parameters to views
Intercept NavigationManager and forward to IViewManager
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>0.2.2</Version>
|
||||
<Version>0.2.3</Version>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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))}))";
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user