Skip to main content

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);
}
}

Standard Navigation Flow

MainView (Container)
├── Primary View (Default/Home)
│ ├── Secondary Views (Sub-workflows)
│ │ └── Modal Dialogs (User interactions)
│ └── Completion Views (Results/Summary)
└── Settings/Configuration Views
  1. Navigation Request: User action or system event triggers navigation
  2. Validation: Guards check if navigation is allowed
  3. NavigatedFrom: Current view receives notification it's being left
  4. Content Switch: MainViewModel updates MainContent property
  5. NavigatedTo: New view receives notification it's being activated
  6. Initialization: New view performs any required setup
  7. 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

  1. Separation of Concerns: Keep navigation logic separate from business logic
  2. Dependency Injection: Always inject navigation services, never instantiate directly
  3. Event Aggregation: Use loose coupling between navigation triggers and handlers
  4. Async/Await: Use non-blocking patterns for navigation operations
  5. Logging: Log all navigation events for debugging
  6. Guards: Implement validation before allowing navigation
  7. Disposal: Always clean up resources when navigating away
  8. Thread Safety: Use Dispatcher for all UI updates

Common Pitfalls to Avoid

  1. Direct View Instantiation: Always use navigation service for view creation
  2. Synchronous Heavy Operations: Use async patterns during navigation
  3. Missing Disposal: Ensure ViewModels are properly disposed
  4. Thread Violations: Always use Dispatcher for UI updates
  5. Circular Navigation: Implement guards to prevent infinite navigation loops
  6. Memory Leaks: Unsubscribe from events when navigating away
  7. 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:

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.