Navigation Architecture Pattern
Overview
The MepDash navigation architecture provides a sophisticated, service-based navigation system built on WPF/MVVM principles. This pattern enables seamless transitions between views while maintaining data integrity, user context, and proper lifecycle management.
Key Concepts
- Service-Based Navigation: Leverages dependency injection for navigation services
- Hierarchical Navigation Model: Multi-tier navigation structure with container views
- Event-Driven Navigation: Uses event aggregation for decoupled navigation triggers
- State Preservation: Implements INavigationTarget for lifecycle management
- View-First Approach: Views are registered and discovered through service providers
Core Components
1. Navigation Service (IBasicMepNavigationService)
The central navigation service provides type-safe navigation with dependency injection support:
public interface IBasicMepNavigationService
{
void NavigateToNewUserControl<TView>() where TView : UserControl;
void NavigateToExistingUserControl(UserControl view);
}
Usage Example:
var navigationService = MainView.MepPluginServiceProvider.GetService<IBasicMepNavigationService>();
navigationService.NavigateToNewUserControl<TargetView>();
2. BaseRouteableViewModel Pattern
All navigable ViewModels inherit from BaseRouteableViewModel:
public abstract class BaseRouteableViewModel : BaseViewModel, INavigationTarget
{
public virtual void NavigatedTo() { }
public virtual void NavigatedFrom() { }
public string Title { get; set; }
}
Features:
- Consistent navigation lifecycle hooks
- Title management for view identification
- INotifyPropertyChanged implementation through BaseViewModel
- Parameter handling for data passing between views
3. MainViewModel as Navigation Host
The MainViewModel serves as the primary navigation container:
public class MainViewModel : BaseRouteableViewModel
{
private readonly BasicMepNavigationUiHostNotificator _navigationUiHostNotificator;
public MainViewModel(BasicMepNavigationUiHostNotificator navigationUiHostNotificator)
{
_navigationUiHostNotificator = navigationUiHostNotificator;
_navigationUiHostNotificator.MainContentChanged += (sender, e) => SetMainContent(sender);
}
public void SetMainContent(object content)
{
Dispatcher.CurrentDispatcher.Invoke(() =>
{
var navigatedFromContent = MainContent;
MainContent = content;
if (navigatedFromContent is INavigationTarget)
((INavigationTarget) navigatedFromContent)?.NavigatedFrom();
if (content is INavigationTarget)
((INavigationTarget) content)?.NavigatedTo();
});
}
}
4. Navigation UI Host Notificator
Manages communication between navigation service and UI container:
public class BasicMepNavigationUiHostNotificator
{
public event EventHandler<EventArgs> MainContentChanged;
public void NotifyContentChanged(object newContent)
{
MainContentChanged?.Invoke(newContent, EventArgs.Empty);
}
}
Navigation Flow Architecture
Standard Navigation Flow
MainView (Container)
├── Primary View (Default/Home)
│ ├── Secondary Views (Sub-workflows)
│ │ └── Modal Dialogs (User interactions)
│ └── Completion Views (Results/Summary)
└── Settings/Configuration Views
Navigation Lifecycle
- Navigation Request: User action or system event triggers navigation
- Validation: Guards check if navigation is allowed
- NavigatedFrom: Current view receives notification it's being left
- Content Switch: MainViewModel updates MainContent property
- NavigatedTo: New view receives notification it's being activated
- Initialization: New view performs any required setup
- Render: UI updates to show new content
Implementation Patterns
Pattern 1: Service Registration
Register navigation services through dependency injection:
public class RegisterServiceProvider : ARegisterService
{
public override void RegisterServices(IServiceCollection services)
{
services.AddTransient<IBasicMepNavigationService, BasicMepNavigationService>();
services.AddSingleton<BasicMepNavigationUiHostNotificator>();
// Register navigation helpers
services.AddTransient<NavToOrderCompletion>();
}
}
Pattern 2: Event-Driven Navigation
Decouple navigation triggers from navigation logic:
public class SourceViewModel : BaseRouteableViewModel
{
public event EventHandler<EventArgs> NavigateToTarget;
private async Task TriggerNavigation()
{
IsLoading = true;
await Task.Delay(300).ConfigureAwait(true);
NavigateToTarget?.Invoke(this, new EventArgs());
}
}
Pattern 3: Navigation with Parameters
Pass data between views during navigation:
public void NavigateWithData(object data, RelayCommandAsync backCommand)
{
var targetView = _serviceProvider.GetService<TargetView>();
var targetViewModel = targetView.DataContext as TargetViewModel;
targetViewModel.SetData(data);
targetViewModel.NavigateBackCmd = backCommand;
_navigationService.NavigateToExistingUserControl(targetView);
}
Pattern 4: Navigation Guards
Implement validation before allowing navigation:
public async Task<bool> CanNavigate()
{
// Validate required data
if (RequiredData == null || !RequiredData.Any())
return false;
// Check business rules
if (!await ValidateBusinessRules())
return false;
// Check for unsaved changes
if (HasUnsavedChanges)
{
var result = await ShowConfirmationDialog();
if (!result)
return false;
}
return true;
}
Pattern 5: Back Navigation
Handle back navigation with state cleanup:
public class TargetViewModel : BaseRouteableViewModel
{
private RelayCommandAsync _navigateBackCmd;
public RelayCommandAsync NavigateBackCmd
{
get => _navigateBackCmd;
set
{
if (_navigateBackCmd != value)
{
_navigateBackCmd = value;
OnPropertyChanged(nameof(NavigateBackCmd));
}
}
}
public override void NavigatedFrom()
{
// Clean up resources
ClearData();
UnsubscribeEvents();
base.NavigatedFrom();
}
}
Thread Safety and UI Synchronization
All navigation operations must be synchronized with the UI thread:
public void SafeNavigate(object content)
{
Dispatcher.CurrentDispatcher.Invoke(() =>
{
if (_logger.IsEnabled(LogLevel.Trace))
{
_logger.LogTrace("Navigation to {@NavigationContext}",
new { ContentType = content?.GetType().Name });
}
try
{
MainContent = content;
}
catch (Exception ex)
{
_logger.LogError(ex, "Navigation failed");
HandleNavigationError(ex);
}
});
}
Performance Optimization
1. Lazy Loading
private Lazy<TargetView> _targetView;
public void NavigateToTarget()
{
var view = _targetView.Value; // View created only when needed
_navigationService.NavigateToExistingUserControl(view);
}
2. View Caching
private readonly Dictionary<Type, UserControl> _viewCache = new();
public TView GetOrCreateView<TView>() where TView : UserControl
{
if (!_viewCache.TryGetValue(typeof(TView), out var cachedView))
{
cachedView = _serviceProvider.GetService<TView>();
_viewCache[typeof(TView)] = cachedView;
}
return (TView)cachedView;
}
3. Async Navigation
public async Task NavigateAsync<TView>() where TView : UserControl
{
IsLoading = true;
await Task.Run(() =>
{
// Prepare data on background thread
PrepareNavigationData();
});
await Dispatcher.CurrentDispatcher.InvokeAsync(() =>
{
_navigationService.NavigateToNewUserControl<TView>();
IsLoading = false;
});
}
Memory Management
Proper Disposal Pattern
public class NavigableViewModel : BaseRouteableViewModel, IDisposable
{
private bool _disposed = false;
public override void NavigatedFrom()
{
Dispose();
base.NavigatedFrom();
}
public void Dispose()
{
if (!_disposed)
{
// Unsubscribe from events
if (_eventAggregator != null)
_eventAggregator.UnsubscribeAll(this);
// Clear collections
LargeDataCollection?.Clear();
// Dispose managed resources
_cancellationTokenSource?.Dispose();
_disposed = true;
}
}
}
Error Handling
Comprehensive Navigation Error Handling
public async Task SafeNavigateAsync<TView>() where TView : UserControl
{
try
{
if (!await CanNavigate())
{
_logger.LogWarning("Navigation blocked by guard");
return;
}
_navigationService.NavigateToNewUserControl<TView>();
}
catch (ServiceNotFoundException ex)
{
_logger.LogError(ex, "Navigation service not found");
await ShowErrorDialog("Navigation service unavailable");
}
catch (ViewNotFoundException ex)
{
_logger.LogError(ex, "Target view not found");
await ShowErrorDialog($"View {typeof(TView).Name} not found");
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected navigation error");
await ShowErrorDialog("Navigation failed");
}
}
Best Practices
- Separation of Concerns: Keep navigation logic separate from business logic
- Dependency Injection: Always inject navigation services, never instantiate directly
- Event Aggregation: Use loose coupling between navigation triggers and handlers
- Async/Await: Use non-blocking patterns for navigation operations
- Logging: Log all navigation events for debugging
- Guards: Implement validation before allowing navigation
- Disposal: Always clean up resources when navigating away
- Thread Safety: Use Dispatcher for all UI updates
Common Pitfalls to Avoid
- Direct View Instantiation: Always use navigation service for view creation
- Synchronous Heavy Operations: Use async patterns during navigation
- Missing Disposal: Ensure ViewModels are properly disposed
- Thread Violations: Always use Dispatcher for UI updates
- Circular Navigation: Implement guards to prevent infinite navigation loops
- Memory Leaks: Unsubscribe from events when navigating away
- Lost State: Preserve necessary state before navigation
Testing Navigation
Unit Testing Example
[Test]
public async Task NavigationService_NavigatesToCorrectView()
{
// Arrange
var mockNavigationService = new Mock<IBasicMepNavigationService>();
var viewModel = new TestViewModel(mockNavigationService.Object);
// Act
await viewModel.NavigateToTargetAsync();
// Assert
mockNavigationService.Verify(x =>
x.NavigateToNewUserControl<TargetView>(), Times.Once);
}
Used In
This navigation architecture pattern is implemented in:
Related Patterns
Summary
The MepDash Navigation Architecture provides a robust, maintainable, and performant framework for managing view transitions. By leveraging service-based navigation, event-driven updates, and proper lifecycle management, the system ensures smooth user experiences while maintaining code quality and testability. The architecture supports both linear workflows and complex navigation patterns, making it suitable for enterprise-grade applications.