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)
{