A robust .NET library for implementing navigation stacks with undo/redo support and persistence capabilities.
ktsu.Navigation provides a complete navigation stack implementation that supports:
- Forward/Backward Navigation: Navigate through items with full browser-like back/forward functionality
- Undo/Redo Operations: Complete undo/redo support for all navigation actions
- Persistence: Save and restore navigation state to various storage backends
- Event-Driven Architecture: React to navigation changes with comprehensive event notifications
- Generic Design: Works with any navigation item type implementing
INavigationItem - Factory Pattern: Easy creation of navigation stacks with different configurations
Perfect for applications that need robust navigation management, such as:
- Code editors with file navigation
- Image viewers with browsing history
- Document management systems
- Any application requiring navigation state management
- ✅ NavigateTo, GoBack, GoForward operations
- ✅ Current item tracking with CanGoBack/CanGoForward status
- ✅ Complete navigation history management
- ✅ Forward history clearing when navigating to new items
- ✅ Thread-safe operations
- ✅ Full undo/redo support for navigation operations
- ✅ Configurable history size limits
- ✅ State change notifications
- ✅ Error handling during undo/redo operations
- ✅ Redirects made by a
NavigationChangedhandler during an undo or redo are not recorded as new actions, so the redo history survives them
- ✅ JSON file persistence provider
- ✅ In-memory persistence for testing
- ✅ Async operations with cancellation token support
- ✅ Custom persistence provider support
- ✅ Navigation change events (NavigateTo, GoBack, GoForward, Clear)
- ✅ Metadata support for navigation items
- ✅ Generic design for custom navigation item types
- ✅ Factory pattern for easy configuration
Add the NuGet package:
dotnet add package ktsu.Navigation
using ktsu.Navigation.Models; using ktsu.Navigation.Services; // Create navigation items var page1 = new NavigationItem("page1", "Home Page"); var page2 = new NavigationItem("page2", "About Page"); var page3 = new NavigationItem("page3", "Contact Page"); // Create a navigation stack var navigation = new Navigation<NavigationItem>(); // Navigate through items navigation.NavigateTo(page1); // Current: Home Page navigation.NavigateTo(page2); // Current: About Page navigation.NavigateTo(page3); // Current: Contact Page // Go back and forward var previous = navigation.GoBack(); // Current: About Page var next = navigation.GoForward(); // Current: Contact Page // Check navigation state Console.WriteLine($"Current: {navigation.Current?.DisplayName}"); Console.WriteLine($"Can go back: {navigation.CanGoBack}"); Console.WriteLine($"Can go forward: {navigation.CanGoForward}");
using ktsu.Navigation.Services; // Create navigation with undo/redo support var undoRedoProvider = new SimpleUndoRedoProvider(maxHistorySize: 50); var navigation = new Navigation<NavigationItem>(undoRedoProvider); navigation.NavigateTo(page1); navigation.NavigateTo(page2); // Undo the last navigation undoRedoProvider.Undo(); // Back to page1 // Redo the navigation undoRedoProvider.Redo(); // Forward to page2 again
using ktsu.Navigation.Services; // Create navigation with JSON file persistence var persistenceProvider = new JsonFilePersistenceProvider<NavigationItem>("navigation-state.json"); var navigation = new Navigation<NavigationItem>(undoRedoProvider: null, persistenceProvider: persistenceProvider); navigation.NavigateTo(page1); navigation.NavigateTo(page2); // Save navigation state await navigation.SaveStateAsync(); // Later... restore navigation state await navigation.LoadStateAsync(); Console.WriteLine($"Restored to: {navigation.Current?.DisplayName}");
JsonFilePersistenceProvider<T> writes each item as T. If a Navigation<NavigationItem> holds a subclass such as DocumentPage : NavigationItem, JSON can't save its type or the properties it adds. So SaveStateAsync throws NotSupportedException naming the subclass, and the last save stays in place. To keep several page types in one stack, configure polymorphism for T through the JSON options:
using System.Text.Json; using System.Text.Json.Serialization.Metadata; var options = new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, TypeInfoResolver = new DefaultJsonTypeInfoResolver { Modifiers = { typeInfo => { if (typeInfo.Type == typeof(NavigationItem)) { typeInfo.PolymorphismOptions = new JsonPolymorphismOptions { DerivedTypes = { new JsonDerivedType(typeof(DocumentPage), "document") }, }; } }, }, }, }; var persistenceProvider = new JsonFilePersistenceProvider<NavigationItem>("navigation-state.json", options);
Every subclass you save needs a JsonDerivedType entry. Plain NavigationItem items keep working alongside them.
using ktsu.Navigation.Services; // Create providers var undoRedoProvider = new SimpleUndoRedoProvider(); var persistenceProvider = new JsonFilePersistenceProvider<NavigationItem>("app-navigation.json"); // Create navigation with all features var navigation = new Navigation<NavigationItem>(undoRedoProvider, persistenceProvider); // Subscribe to navigation events navigation.NavigationChanged += (sender, e) => { Console.WriteLine($"Navigation: {e.NavigationType}"); Console.WriteLine($"From: {e.PreviousItem?.DisplayName ?? "None"}"); Console.WriteLine($"To: {e.CurrentItem?.DisplayName ?? "None"}"); }; // Use factory for easier creation var factory = new NavigationStackFactory(undoRedoProvider); var anotherNavigation = factory.CreateNavigationStack<NavigationItem>(undoRedoProvider, persistenceProvider);
public class DocumentNavigationItem : INavigationItem { public string Id { get; } public string DisplayName { get; set; } public DateTime CreatedAt { get; } public IReadOnlyDictionary<string, object> Metadata => _metadata.AsReadOnly(); private readonly Dictionary<string, object> _metadata = new(); public DocumentNavigationItem(string filePath, string displayName) : this(filePath, displayName, DateTime.UtcNow, null) { SetMetadata("FilePath", filePath); SetMetadata("FileSize", new FileInfo(filePath).Length); } // JsonFilePersistenceProvider reloads saved items through this constructor. System.Text.Json // needs each parameter to have the same name and type as the property it sets. [JsonConstructor] public DocumentNavigationItem(string id, string displayName, DateTime createdAt, IReadOnlyDictionary<string, object>? metadata) { Id = id; DisplayName = displayName; CreatedAt = createdAt; foreach (var entry in metadata ?? new Dictionary<string, object>()) { _metadata[entry.Key] = entry.Value; } } public void SetMetadata(string key, object value) => _metadata[key] = value; public bool RemoveMetadata(string key) => _metadata.Remove(key); } // Use with navigation var navigation = new Navigation<DocumentNavigationItem>(); var document = new DocumentNavigationItem("/path/to/file.txt", "My Document"); navigation.NavigateTo(document);
The library follows clean architecture principles with well-separated concerns:
- Models:
NavigationItem,NavigationState- Core data structures - Contracts: Interfaces defining the contracts for all components
- Services: Implementation classes for navigation, persistence, and undo/redo
INavigation<T>: Main navigation operations interfaceINavigationItem: Contract for items that can be navigated toIPersistenceProvider<T>: Contract for persistence implementationsIUndoRedoProvider: Contract for undo/redo implementations
Create custom persistence providers by implementing the IPersistenceProvider<T> interface:
public class DatabasePersistenceProvider<T> : IPersistenceProvider<T> where T : INavigationItem { public async Task SaveStateAsync(INavigationState<T> state, CancellationToken cancellationToken = default) { // Save to database await Task.CompletedTask; } public async Task<INavigationState<T>?> LoadStateAsync(CancellationToken cancellationToken = default) { // Load from database return await Task.FromResult<INavigationState<T>?>(null); } // Implement HasSavedState, ClearState, etc. }
Contributions are welcome! Please feel free to submit a Pull Request.
MIT License. Copyright (c) ktsu.dev