diff --git a/Directory.Build.props b/Directory.Build.props index d653cfd..55fd0ec 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@  - 0.2.2 + 0.2.3 enable enable diff --git a/Directory.Packages.props b/Directory.Packages.props index aa1cfbe..b6ae094 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,6 +6,9 @@ + + + \ No newline at end of file diff --git a/README.md b/README.md index 9c7a08b..46fce55 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ public class HomeViewModel : ViewModelBase private void NavigateToProfile() { - this.ViewManager.ShowView(); + this.ViewManager.ShowView(); } } ``` @@ -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(userData); +// Navigate to a view with optional parameters +ViewManager.ShowView([ {"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(object? dataContext)` - Navigate with strong typing -- `ShowView(Type viewType, object? dataContext)` - Navigate by type +- `ShowView(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 diff --git a/TrailBlazr/Components/ViewContainer.razor b/TrailBlazr/Components/ViewContainer.razor index d275898..b7a729c 100644 --- a/TrailBlazr/Components/ViewContainer.razor +++ b/TrailBlazr/Components/ViewContainer.razor @@ -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() { { "DataContext", request.DataContext ?? new object() } }; + this.CurrentParameters = request.RouteValues.ToDictionary(); this.CurrentViewType = request.ViewType; this.StateHasChanged(); } diff --git a/TrailBlazr/Models/ViewRegistration.cs b/TrailBlazr/Models/ViewRegistration.cs index dee8a4e..27c752b 100644 --- a/TrailBlazr/Models/ViewRegistration.cs +++ b/TrailBlazr/Models/ViewRegistration.cs @@ -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 routes) { public Type ViewType { get; } = viewType ?? throw new ArgumentNullException(nameof(viewType)); public Type ViewModelType { get; } = viewModelType ?? throw new ArgumentNullException(nameof(viewModelType)); + public IReadOnlyList Routes { get; } = routes ?? throw new ArgumentNullException(nameof(routes)); } -public sealed class ViewRegistration() : ViewRegistration(typeof(TView), typeof(TViewModel)) +public sealed class ViewRegistration() + : ViewRegistration( + typeof(TView), + typeof(TViewModel), + [.. typeof(TView).GetCustomAttributes().Select(r => r.Template).Select(r => new TemplateMatcher(TemplateParser.Parse(r), []))]) where TView : Views.ViewBase where TViewModel : ViewModels.ViewModelBase { - 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))}))"; } diff --git a/TrailBlazr/Models/ViewRequest.cs b/TrailBlazr/Models/ViewRequest.cs index 194b6ca..6a7e48c 100644 --- a/TrailBlazr/Models/ViewRequest.cs +++ b/TrailBlazr/Models/ViewRequest.cs @@ -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) { } diff --git a/TrailBlazr/Services/IViewManager.cs b/TrailBlazr/Services/IViewManager.cs index d5a1151..f875477 100644 --- a/TrailBlazr/Services/IViewManager.cs +++ b/TrailBlazr/Services/IViewManager.cs @@ -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? ShowViewRequested; - void ShowView(object? dataContext = null) + void ShowView(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); } diff --git a/TrailBlazr/Services/ViewManager.cs b/TrailBlazr/Services/ViewManager.cs index ba35158..01acef5 100644 --- a/TrailBlazr/Services/ViewManager.cs +++ b/TrailBlazr/Services/ViewManager.cs @@ -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? ShowViewRequested; - public void ShowView(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(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(); + 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(); + 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); + } } diff --git a/TrailBlazr/TrailBlazr.csproj b/TrailBlazr/TrailBlazr.csproj index 9d06db9..1a7b884 100644 --- a/TrailBlazr/TrailBlazr.csproj +++ b/TrailBlazr/TrailBlazr.csproj @@ -12,6 +12,9 @@ + + + diff --git a/TrailBlazr/ViewModels/ViewModelBase.cs b/TrailBlazr/ViewModels/ViewModelBase.cs index 3d36a8d..976a492 100644 --- a/TrailBlazr/ViewModels/ViewModelBase.cs +++ b/TrailBlazr/ViewModels/ViewModelBase.cs @@ -9,16 +9,6 @@ public abstract class ViewModelBase : 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; diff --git a/TrailBlazr/Views/ViewBase.cs b/TrailBlazr/Views/ViewBase.cs index 91cf843..d7274b3 100644 --- a/TrailBlazr/Views/ViewBase.cs +++ b/TrailBlazr/Views/ViewBase.cs @@ -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 : ComponentBase, INotifyPropertyChanged +public abstract class ViewBase : ComponentBase where TView : ViewBase where TViewModel : ViewModelBase { public readonly static TimeSpan InitializationTimeout = TimeSpan.FromSeconds(5); - public event PropertyChangedEventHandler? PropertyChanged; - [Inject] public TViewModel ViewModel { get; set; } = default!; [Inject] protected ILogger 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) {