Skip to main content

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

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>

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

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

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

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.