Skip to main content

Navigation Best Practices

Overview

This document outlines the best practices for implementing and maintaining navigation patterns in the EFT Remittance Dashboard. These guidelines ensure consistent user experience, maintainable code, and optimal performance while avoiding common pitfalls that can lead to memory leaks, poor performance, or confusing user interactions.

Key Concepts

  • Memory Management: Proper disposal of resources during navigation
  • State Consistency: Maintaining coherent state across navigation events
  • Performance Optimization: Efficient navigation and view loading
  • User Experience: Smooth, predictable navigation patterns
  • Testability: Navigation patterns that support unit and integration testing

Implementation Details

Memory Management During Navigation

Proper memory management is critical in singleton-based navigation:

public class BaseRouteableViewModel : IDisposable
{
private bool _disposed = false;

public virtual Task NavigatedFromAsync(INavigationRequest request)
{
// Unsubscribe from events
UnsubscribeEvents();

// Cancel pending operations
CancelPendingOperations();

// Clear large collections
ClearCachedData();

return Task.CompletedTask;
}

protected virtual void UnsubscribeEvents()
{
// Unsubscribe from service events
if (_service != null)
{
_service.DataChanged -= OnDataChanged;
_service.ProgressUpdated -= OnProgressUpdated;
}
}

protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose managed resources
UnsubscribeEvents();
_cancellationTokenSource?.Dispose();
}
_disposed = true;
}
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}

State Management Best Practices

public class ViewModelStateManager
{
private readonly Dictionary<string, object> _stateCache = new();

public void SaveState(string key, object state)
{
_stateCache[key] = state;

// Implement size limits
if (_stateCache.Count > 100)
{
// Remove oldest entries
var oldestKey = _stateCache.Keys.First();
_stateCache.Remove(oldestKey);
}
}

public T RestoreState<T>(string key, T defaultValue = default)
{
if (_stateCache.TryGetValue(key, out var state) && state is T typedState)
{
return typedState;
}
return defaultValue;
}

public void ClearState(string prefix = null)
{
if (string.IsNullOrEmpty(prefix))
{
_stateCache.Clear();
}
else
{
var keysToRemove = _stateCache.Keys
.Where(k => k.StartsWith(prefix))
.ToList();

foreach (var key in keysToRemove)
{
_stateCache.Remove(key);
}
}
}
}
public interface INavigationGuard
{
Task<bool> CanNavigateAsync(NavigationContext context);
}

public class UnsavedChangesGuard : INavigationGuard
{
private readonly IDialogService _dialogService;

public async Task<bool> CanNavigateAsync(NavigationContext context)
{
if (context.ViewModel is IHasUnsavedChanges viewModel &&
viewModel.HasUnsavedChanges)
{
var result = await _dialogService.ShowConfirmationAsync(
"You have unsaved changes. Do you want to continue?",
"Unsaved Changes");

return result;
}

return true;
}
}

Examples

Example 1: Proper Event Subscription Management

public class EftRemittanceViewModel : BaseViewModel
{
private readonly IEftRemittanceService _service;
private readonly WeakEventManager _weakEventManager = new();

public override Task NavigatedToAsync(INavigationRequest request)
{
// Use weak event patterns to prevent memory leaks
WeakEventManager<IEftRemittanceService, EventArgs>
.AddHandler(_service, nameof(_service.DataUpdated), OnDataUpdated);

return base.NavigatedToAsync(request);
}

public override Task NavigatedFromAsync(INavigationRequest request)
{
// Always unsubscribe when navigating away
WeakEventManager<IEftRemittanceService, EventArgs>
.RemoveHandler(_service, nameof(_service.DataUpdated), OnDataUpdated);

return base.NavigatedFromAsync(request);
}

private void OnDataUpdated(object sender, EventArgs e)
{
// Handle event
RefreshData();
}
}

Example 2: Async Navigation Pattern

public class NavigationService
{
private readonly SemaphoreSlim _navigationSemaphore = new(1, 1);

public async Task NavigateAsync(string route, object parameters = null)
{
// Prevent concurrent navigation
await _navigationSemaphore.WaitAsync();

try
{
// Check if current view can navigate
if (_currentViewModel is INavigationAware current)
{
var canNavigate = await current.CanNavigateFromAsync();
if (!canNavigate)
return;
}

// Perform navigation
var nextViewModel = ResolveViewModel(route);

// Initialize new view model
if (nextViewModel is INavigationAware next)
{
await next.NavigatedToAsync(new NavigationRequest
{
Parameters = parameters
});
}

// Update current view model
_currentViewModel = nextViewModel;
}
finally
{
_navigationSemaphore.Release();
}
}
}

Example 3: Navigation Testing Pattern

[TestClass]
public class NavigationTests
{
private Mock<INavigationService> _navigationServiceMock;
private MainViewModel _viewModel;

[TestInitialize]
public void Setup()
{
_navigationServiceMock = new Mock<INavigationService>();
_viewModel = new MainViewModel(_navigationServiceMock.Object);
}

[TestMethod]
public async Task NavigatedTo_LoadsData_WhenParametersProvided()
{
// Arrange
var request = new NavigationRequest
{
Parameters = new Dictionary<string, object>
{
["PaymentNumber"] = "PMT001"
}
};

// Act
await _viewModel.NavigatedToAsync(request);

// Assert
Assert.IsNotNull(_viewModel.SelectedPayment);
Assert.AreEqual("PMT001", _viewModel.SelectedPayment.Value);
}

[TestMethod]
public async Task NavigatedFrom_ClearsResources_Always()
{
// Arrange
await _viewModel.LoadDataAsync();

// Act
await _viewModel.NavigatedFromAsync(new NavigationRequest());

// Assert
Assert.AreEqual(0, _viewModel.PaymentDetails.Count);
Assert.IsNull(_viewModel.SelectedPayment);
}
}

Performance Optimization

Lazy Loading Pattern

public class LazyNavigationViewModel : BaseViewModel
{
private readonly Lazy<HeavyResource> _heavyResource;

public LazyNavigationViewModel()
{
_heavyResource = new Lazy<HeavyResource>(
() => new HeavyResource(),
LazyThreadSafetyMode.ExecutionAndPublication);
}

public override async Task NavigatedToAsync(INavigationRequest request)
{
// Only initialize heavy resources when actually navigated to
if (_heavyResource.IsValueCreated)
{
await _heavyResource.Value.RefreshAsync();
}

await base.NavigatedToAsync(request);
}
}

View Caching Strategy

public class ViewCache
{
private readonly Dictionary<Type, object> _viewCache = new();
private readonly int _maxCacheSize = 5;

public T GetOrCreateView<T>() where T : class, new()
{
var type = typeof(T);

if (_viewCache.TryGetValue(type, out var cachedView))
{
return (T)cachedView;
}

var newView = new T();

// Implement LRU cache
if (_viewCache.Count >= _maxCacheSize)
{
var oldestEntry = _viewCache.First();
_viewCache.Remove(oldestEntry.Key);
}

_viewCache[type] = newView;
return newView;
}
}

Accessibility Considerations

public class AccessibleNavigationViewModel : BaseViewModel
{
public override async Task NavigatedToAsync(INavigationRequest request)
{
// Announce navigation to screen readers
AutomationProperties.SetLiveSetting(View, AutomationLiveSetting.Assertive);
AutomationProperties.SetName(View, $"Navigated to {ViewTitle}");

// Set focus to first interactive element
await Dispatcher.InvokeAsync(() =>
{
var firstControl = FindFirstFocusableElement();
firstControl?.Focus();
}, DispatcherPriority.Loaded);

await base.NavigatedToAsync(request);
}
}

Error Handling in Navigation

public class SafeNavigationService
{
private readonly ILogger<SafeNavigationService> _logger;

public async Task<bool> TryNavigateAsync(string route, object parameters = null)
{
try
{
await NavigateAsync(route, parameters);
return true;
}
catch (NavigationException ex)
{
_logger.LogError(ex, "Navigation failed. {@NavigationContext}",
new { route, parameters });

// Show user-friendly error
await ShowNavigationError(ex);

return false;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error during navigation. {@NavigationContext}",
new { route, parameters });

// Attempt recovery
await NavigateToErrorView(ex);

return false;
}
}
}

Common Anti-Patterns to Avoid

1. Memory Leak Pattern

// BAD: Strong event subscription without unsubscribe
public class LeakyViewModel
{
public LeakyViewModel(IService service)
{
service.DataChanged += OnDataChanged; // Memory leak!
}
}

// GOOD: Proper event management
public class ProperViewModel : IDisposable
{
private readonly IService _service;

public ProperViewModel(IService service)
{
_service = service;
_service.DataChanged += OnDataChanged;
}

public void Dispose()
{
_service.DataChanged -= OnDataChanged;
}
}

2. Blocking Navigation Pattern

// BAD: Synchronous blocking operation
public void NavigateTo(string route)
{
var data = LoadDataSynchronously(); // Blocks UI!
ShowView(route, data);
}

// GOOD: Async navigation
public async Task NavigateToAsync(string route)
{
ShowLoadingIndicator();
var data = await LoadDataAsync();
HideLoadingIndicator();
await ShowViewAsync(route, data);
}

Best Practices Summary

  1. Always use async/await for navigation operations
  2. Implement IDisposable for proper resource cleanup
  3. Use weak events to prevent memory leaks
  4. Validate navigation with guards before proceeding
  5. Cache views judiciously to balance memory and performance
  6. Log navigation events for debugging and analytics
  7. Handle errors gracefully with fallback navigation
  8. Test navigation flows with unit and integration tests
  9. Consider accessibility in navigation implementations
  10. Document navigation flows for team understanding

Summary

Following these navigation best practices ensures that the EFT Remittance Dashboard maintains high performance, prevents memory leaks, and provides a smooth user experience. The patterns presented here have been proven in production environments and should be applied consistently throughout the application. Regular code reviews should verify adherence to these practices, and any deviations should be documented and justified.