Navigation Best Practices
Overview
This document outlines best practices, anti-patterns, and optimization strategies for navigation in the AR Payment Reversal dashboard. Following these guidelines ensures maintainable, performant, and user-friendly navigation experiences while avoiding common pitfalls that can lead to memory leaks, poor performance, or confusing user experiences.
Key Concepts
- Memory Management: Preventing leaks during navigation
- Performance Optimization: Ensuring smooth transitions
- Accessibility: Supporting keyboard and screen reader navigation
- Error Resilience: Graceful handling of navigation failures
- Testing Strategies: Comprehensive navigation testing approaches
Navigation Anti-Patterns to Avoid
1. Direct View Instantiation
❌ Bad Practice:
// Never create views directly
private void NavigateToAddPayment()
{
var view = new ArReversePaymentAddPaymentView();
var viewModel = new ArReversePaymentAddPaymentViewModel(_logger, _service);
view.DataContext = viewModel;
MainContent = view;
}
✅ Best Practice:
// Always use navigation service
private void NavigateToAddPayment()
{
var navigationService = _serviceProvider.GetService<IBasicMepNavigationService>();
navigationService.NavigateToNewUserControl<ArReversePaymentAddPaymentView>();
}
Why: Direct instantiation bypasses dependency injection, making testing difficult and preventing proper lifecycle management.
2. Synchronous Heavy Operations
❌ Bad Practice:
public void NavigatedTo()
{
// Blocking UI thread with synchronous operation
var data = _service.LoadLargeDataSet();
ProcessData(data);
UpdateUI();
}
✅ Best Practice:
public async void NavigatedTo()
{
try
{
IsLoading = true;
var data = await _service.LoadLargeDataSetAsync();
await ProcessDataAsync(data);
UpdateUI();
}
finally
{
IsLoading = false;
}
}
Why: Synchronous operations block the UI thread, causing freezes and poor user experience.
3. Missing Event Unsubscription
❌ Bad Practice:
public class PaymentViewModel : BaseViewModel
{
public PaymentViewModel()
{
_eventAggregator.Subscribe<PaymentEvent>(OnPaymentReceived);
// Never unsubscribed - memory leak!
}
}
✅ Best Practice:
public class PaymentViewModel : BaseViewModel, IDisposable
{
private bool _disposed;
public PaymentViewModel()
{
_eventAggregator.Subscribe<PaymentEvent>(OnPaymentReceived);
}
public void Dispose()
{
if (!_disposed)
{
_eventAggregator.Unsubscribe<PaymentEvent>(OnPaymentReceived);
_disposed = true;
}
}
}
Why: Unsubscribed events keep objects in memory, causing memory leaks.
4. Circular Navigation
❌ Bad Practice:
// View A navigates to View B
// View B navigates to View C
// View C navigates to View A
// No validation or guards - infinite loop possible
✅ Best Practice:
public class NavigationGuard
{
private readonly Stack<Type> _navigationHistory = new();
public bool CanNavigate(Type targetView)
{
// Prevent immediate re-navigation to same view
if (_navigationHistory.Count > 0 && _navigationHistory.Peek() == targetView)
return false;
// Prevent circular navigation patterns
if (_navigationHistory.Count > 2)
{
var recent = _navigationHistory.Take(3).ToList();
if (recent.Distinct().Count() < recent.Count)
{
_logger.LogWarning("Potential circular navigation detected");
return false;
}
}
return true;
}
}
Memory Management During Navigation
Proper Disposal Pattern
// MepApps.Dash.Ar.Maint.PaymentReversal/ViewModels/ArReversePaymentAddPaymentViewModel.cs
public class ArReversePaymentAddPaymentViewModel : BaseRouteableViewModel, IDisposable
{
private bool _disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose managed resources
this.PropertyChanged -= ArReversePaymentAddPaymentViewModel_PropertyChanged;
if (_customerPayments != null)
{
foreach (var payment in _customerPayments)
{
payment.PropertyChanged -= Payment_PropertyChanged;
}
}
// Clear collections
_customerPayments?.Clear();
_customerPayments = null;
}
_disposed = true;
}
}
}
Weak Event Pattern
For long-lived objects subscribing to events:
public class WeakEventManager<TEventArgs> where TEventArgs : EventArgs
{
private readonly List<WeakReference> _handlers = new();
public void Subscribe(EventHandler<TEventArgs> handler)
{
_handlers.Add(new WeakReference(handler));
}
public void Raise(object sender, TEventArgs e)
{
_handlers.RemoveAll(wr => !wr.IsAlive);
foreach (var wr in _handlers.ToList())
{
if (wr.Target is EventHandler<TEventArgs> handler)
{
handler(sender, e);
}
}
}
}
Performance Optimization
View Caching Strategy
public class ViewCache
{
private readonly Dictionary<Type, WeakReference> _cache = new();
private readonly IViewFactoryService _viewFactory;
public object GetOrCreateView(Type viewType)
{
if (_cache.TryGetValue(viewType, out var weakRef) && weakRef.IsAlive)
{
return weakRef.Target;
}
var view = _viewFactory.CreateView(viewType);
_cache[viewType] = new WeakReference(view);
return view;
}
public void ClearCache()
{
_cache.Clear();
}
}
Lazy Loading Pattern
public class LazyNavigationViewModel : BaseViewModel
{
private readonly Lazy<ObservableCollection<DataItem>> _items;
public LazyNavigationViewModel()
{
_items = new Lazy<ObservableCollection<DataItem>>(() => LoadItems());
}
public ObservableCollection<DataItem> Items => _items.Value;
private ObservableCollection<DataItem> LoadItems()
{
// Expensive operation only executed when needed
return new ObservableCollection<DataItem>(_service.GetItems());
}
}
Virtualization for Large Data Sets
<!-- Enable UI virtualization for better performance -->
<DataGrid
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
ScrollViewer.CanContentScroll="True"
EnableRowVirtualization="True"
EnableColumnVirtualization="True">
</DataGrid>
Navigation Testing Guidelines
Unit Testing Navigation
[TestClass]
public class NavigationTests
{
private Mock<IBasicMepNavigationService> _navigationServiceMock;
private Mock<IArReversePaymentService> _serviceMock;
[TestInitialize]
public void Setup()
{
_navigationServiceMock = new Mock<IBasicMepNavigationService>();
_serviceMock = new Mock<IArReversePaymentService>();
}
[TestMethod]
public async Task Should_Navigate_To_AddPayment_When_Command_Executed()
{
// Arrange
var viewModel = new ArReversePaymentQueueViewModel(_logger, _serviceMock.Object);
bool navigatedToAddPayment = false;
viewModel.NavigateToAddPayment += (s, e) => navigatedToAddPayment = true;
// Act
await viewModel.AddPaymentCmd.ExecuteAsync(null);
// Assert
Assert.IsTrue(navigatedToAddPayment);
}
[TestMethod]
public void Should_Validate_Before_Navigation()
{
// Arrange
var viewModel = new ArReversePaymentQueueViewModel(_logger, _serviceMock.Object);
viewModel.ReversePaymentQueueHeaders = null; // No items
// Act
var canNavigate = viewModel.ReversePaymentsCmd.CanExecute(null);
// Assert
Assert.IsFalse(canNavigate);
}
}
Integration Testing Navigation Flow
[TestClass]
public class NavigationFlowTests
{
[TestMethod]
public async Task Complete_Payment_Reversal_Flow()
{
// Arrange
var serviceProvider = BuildTestServiceProvider();
var navigationService = serviceProvider.GetService<IBasicMepNavigationService>();
// Act & Assert - Navigate to queue
navigationService.NavigateToNewUserControl<ArReversePaymentQueueView>();
Assert.IsInstanceOfType(GetCurrentView(), typeof(ArReversePaymentQueueView));
// Navigate to add payment
var queueViewModel = GetCurrentViewModel<IArReversePaymentQueueViewModel>();
await queueViewModel.AddPaymentCmd.ExecuteAsync(null);
Assert.IsInstanceOfType(GetCurrentView(), typeof(ArReversePaymentAddPaymentView));
// Complete and navigate back
var addPaymentViewModel = GetCurrentViewModel<IArReversePaymentAddPaymentViewModel>();
addPaymentViewModel.NavigateBackCmd.Execute(null);
Assert.IsInstanceOfType(GetCurrentView(), typeof(ArReversePaymentQueueView));
}
}
Accessibility Considerations
Keyboard Navigation Support
public class KeyboardNavigationBehavior
{
public static void AttachKeyboardShortcuts(UIElement element, INavigationService navigationService)
{
element.PreviewKeyDown += (sender, e) =>
{
if (Keyboard.Modifiers == ModifierKeys.Control)
{
switch (e.Key)
{
case Key.N:
navigationService.NavigateToNewPayment();
e.Handled = true;
break;
case Key.Q:
navigationService.NavigateToQueue();
e.Handled = true;
break;
case Key.Escape:
navigationService.NavigateBack();
e.Handled = true;
break;
}
}
};
}
}
Screen Reader Support
<!-- Provide automation properties for screen readers -->
<Button
x:Name="NavigateButton"
AutomationProperties.Name="Navigate to Add Payment"
AutomationProperties.HelpText="Opens the add payment screen to add new payments to the reversal queue"
AutomationProperties.AutomationId="NavigateToAddPaymentButton">
Add Payment
</Button>
Error Handling During Navigation
Comprehensive Error Recovery
public class ResilientNavigationService : IBasicMepNavigationService
{
private readonly ILogger<ResilientNavigationService> _logger;
private readonly IViewFactoryService _viewFactory;
private object _lastValidView;
public void NavigateToNewUserControl<T>() where T : class
{
try
{
// Store current view for rollback
_lastValidView = GetCurrentView();
var newView = _viewFactory.CreateView<T>();
SetCurrentView(newView);
_logger.LogInformation("Successfully navigated to {ViewType}", typeof(T).Name);
}
catch (Exception ex)
{
_logger.LogError(ex, "Navigation to {ViewType} failed", typeof(T).Name);
// Rollback to last valid view
if (_lastValidView != null)
{
SetCurrentView(_lastValidView);
_logger.LogInformation("Rolled back to previous view");
}
// Notify user
ShowErrorNotification($"Unable to navigate to {typeof(T).Name}: {ex.Message}");
}
}
}
Performance Monitoring
Navigation Timing
public class NavigationPerformanceMonitor
{
private readonly ILogger<NavigationPerformanceMonitor> _logger;
private readonly Dictionary<string, Stopwatch> _timings = new();
public void StartNavigation(string viewName)
{
_timings[viewName] = Stopwatch.StartNew();
}
public void CompleteNavigation(string viewName)
{
if (_timings.TryGetValue(viewName, out var stopwatch))
{
stopwatch.Stop();
var elapsed = stopwatch.ElapsedMilliseconds;
_logger.LogInformation("Navigation to {ViewName} took {ElapsedMs}ms", viewName, elapsed);
if (elapsed > 500)
{
_logger.LogWarning("Slow navigation detected for {ViewName}: {ElapsedMs}ms", viewName, elapsed);
}
_timings.Remove(viewName);
}
}
}
Navigation State Persistence
Saving Navigation State
public class NavigationStateManager
{
private readonly string _stateFile = "navigation-state.json";
public void SaveState(NavigationState state)
{
var json = JsonSerializer.Serialize(state);
File.WriteAllText(_stateFile, json);
}
public NavigationState LoadState()
{
if (File.Exists(_stateFile))
{
var json = File.ReadAllText(_stateFile);
return JsonSerializer.Deserialize<NavigationState>(json);
}
return new NavigationState();
}
}
public class NavigationState
{
public string LastView { get; set; }
public Dictionary<string, object> ViewStates { get; set; } = new();
public DateTime LastNavigationTime { get; set; }
}
Checklist for Navigation Implementation
Before implementing navigation:
- Use dependency injection for view creation
- Implement IDisposable for ViewModels with events
- Add loading indicators for async operations
- Include error handling and recovery
- Validate navigation preconditions
- Support keyboard navigation
- Add automation properties for accessibility
- Log navigation events for debugging
- Test navigation flows
- Monitor performance metrics
- Document navigation patterns
- Handle back navigation properly
- Clear sensitive data when navigating away
Related Documentation
- Navigation Architecture - Overall system design
- Page Routing - Route configuration
- Navigation Events - Event patterns
- Dialog Navigation - Modal dialogs
Summary
Following these best practices ensures the AR Payment Reversal dashboard maintains high performance, accessibility, and reliability in its navigation system. By avoiding anti-patterns, implementing proper memory management, and following testing guidelines, developers can create navigation experiences that are both user-friendly and maintainable.