Navigation Events
Overview
The AR Payment Reversal dashboard implements a comprehensive event-driven navigation system that decouples navigation triggers from navigation logic. This document details all navigation-related events, their usage patterns, and how they facilitate clean, maintainable navigation flows throughout the application.
Key Concepts
- Event Aggregation: Loose coupling between event publishers and subscribers
- Custom EventArgs: Strongly-typed event data transfer
- Event Bubbling: Hierarchical event propagation
- Async Event Handling: Non-blocking event processing
- Event-Command Bridge: Integration with MVVM command pattern
Implementation Details
Core Event Infrastructure
Event Model Hierarchy
// MepApps.Dash.Ar.Maint.PaymentReversal/Models/EventArgModels/
public abstract class BaseEventArgs : EventArgs
{
public DateTime Timestamp { get; }
public string Source { get; }
protected BaseEventArgs(string source)
{
Timestamp = DateTime.Now;
Source = source;
}
}
Navigation-Related Events
1. NotificationEventArgs
Used for general notifications that may trigger navigation based on user response:
// MepApps.Dash.Ar.Maint.PaymentReversal/Models/EventArgModels/NotificationEventArgs.cs
public class NotificationEventArgs : EventArgs
{
public string Title { get; set; }
public string Message { get; set; }
public NotificationType Type { get; set; }
public Action OnConfirm { get; set; }
public Action OnCancel { get; set; }
public NotificationEventArgs(string title, string message,
NotificationType type = NotificationType.Information)
{
Title = title;
Message = message;
Type = type;
}
}
public enum NotificationType
{
Information,
Warning,
Error,
Confirmation
}
Usage Example:
// MepApps.Dash.Ar.Maint.PaymentReversal/ViewModels/ArReversePaymentQueueViewModel.cs (Lines 364-367)
catch (Exception ex)
{
_logger.LogError(ex, "Error in ReversePaymentsAsync");
NotificationEvent?.Invoke(this, new NotificationEventArgs("Error in ReversePaymentsAsync", ex.Message));
}
2. CustomerSelectedEventArgs
Triggers navigation with customer context:
// MepApps.Dash.Ar.Maint.PaymentReversal/Models/EventArgModels/CustomerSelectedEventArgs.cs
public class CustomerSelectedEventArgs : EventArgs
{
public string Customer { get; set; }
public string CustomerName { get; set; }
public bool NavigateToDetails { get; set; }
public CustomerSelectedEventArgs(string customer, string customerName,
bool navigateToDetails = false)
{
Customer = customer;
CustomerName = customerName;
NavigateToDetails = navigateToDetails;
}
}
3. ReversePaymentPostEventArgs
Carries completion data for post-processing navigation:
// MepApps.Dash.Ar.Maint.PaymentReversal/Models/EventArgModels/ReversePaymentPostEventArgs.cs
public class ReversePaymentPostEventArgs : EventArgs
{
public ArReversePaymentPostCompletion CompletionObject { get; }
public bool AutoNavigate { get; set; }
public ReversePaymentPostEventArgs(ArReversePaymentPostCompletion completionObject,
bool autoNavigate = true)
{
CompletionObject = completionObject;
AutoNavigate = autoNavigate;
}
}
Event-Driven Navigation Patterns
Pattern 1: Direct Navigation Events
Simple events that directly trigger navigation:
// MepApps.Dash.Ar.Maint.PaymentReversal/ViewModels/ArReversePaymentQueueViewModel.cs (Lines 31-33, 259-272)
public event EventHandler<EventArgs> NavigateToAddPayment;
private async Task AddPayment()
{
try
{
IsLoading = true;
await Task.Delay(300).ConfigureAwait(true);
NavigateToAddPayment?.Invoke(this, new EventArgs());
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in AddPayment");
NotificationEvent?.Invoke(this, new NotificationEventArgs("Error in AddPayment", ex.Message));
}
}
View subscription:
// In the View code-behind
viewModel.NavigateToAddPayment += (sender, e) =>
{
var addPaymentView = _viewFactory.CreateView<ArReversePaymentAddPaymentView>();
_navigationService.NavigateTo(addPaymentView);
};
Pattern 2: Conditional Navigation Events
Events that include navigation decision logic:
// MepApps.Dash.Ar.Maint.PaymentReversal/ViewModels/ArReversePaymentQueueViewModel.cs (Lines 338-372)
public event EventHandler<ReversePaymentPostEventArgs> PostCompletedEvent;
private async Task ReversePaymentsAsync()
{
try
{
if (ReversePaymentQueueHeaders == null || !ReversePaymentQueueHeaders.Any())
return;
// ... processing logic ...
ArReversePaymentPostCompletion completionObject =
await _service.ReversePaymentsAsync(ReversePaymentQueueHeaders, invoices, _selectedPostPeriod.Value);
if (completionObject == null)
{
throw new Exception("CompletionObject is NULL ReversePaymentsAsync");
}
else
{
ClearPaymentsCmd.Execute(null);
PostCompletedEvent?.Invoke(this, new ReversePaymentPostEventArgs(completionObject));
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in ReversePaymentsAsync");
NotificationEvent?.Invoke(this, new NotificationEventArgs("Error in ReversePaymentsAsync", ex.Message));
}
}
Pattern 3: Event Chains
Events that trigger cascading navigation:
public class NavigationChainExample
{
public event EventHandler<EventArgs> Step1Completed;
public event EventHandler<EventArgs> Step2Completed;
public event EventHandler<EventArgs> ChainCompleted;
public async Task ExecuteChain()
{
// Step 1
await ProcessStep1();
Step1Completed?.Invoke(this, EventArgs.Empty);
// Step 2
await ProcessStep2();
Step2Completed?.Invoke(this, EventArgs.Empty);
// Chain complete
ChainCompleted?.Invoke(this, EventArgs.Empty);
}
}
Event Bubbling and Handling Strategies
Hierarchical Event Propagation
// MepApps.Dash.Ar.Maint.PaymentReversal/ViewModels/MainViewModel.cs (Lines 24-26)
_navigationUiHostNotificator.MainContentChanged += (sender, e) => SetMainContent(sender);
Events bubble up from child ViewModels to parent containers:
ArReversePaymentAddPaymentViewModel
↓ (NavigateBackEvent)
ArReversePaymentQueueView
↓ (MainContentChanged)
MainViewModel
↓ (Updates UI)
MainView
Event Subscription Management
Proper subscription and unsubscription to prevent memory leaks:
// MepApps.Dash.Ar.Maint.PaymentReversal/ViewModels/ArReversePaymentAddPaymentViewModel.cs (Lines 45, 366-378)
public ArReversePaymentAddPaymentViewModel(...)
{
this.PropertyChanged += ArReversePaymentAddPaymentViewModel_PropertyChanged;
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
this.PropertyChanged -= ArReversePaymentAddPaymentViewModel_PropertyChanged;
if (_customerPayments != null)
{
foreach (var check in _customerPayments)
check.PropertyChanged -= Payment_PropertyChanged;
}
}
}
Event Handling Patterns
Async Event Handling
// MepApps.Dash.Ar.Maint.PaymentReversal/ViewModels/ArReversePaymentQueueViewModel.cs (Lines 79-95)
private async void ArReversePaymentQueueViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
try
{
switch (e.PropertyName)
{
case "ReversePaymentQueueHeaders":
await ReversePaymentQueueHeadersChanged().ConfigureAwait(true);
break;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in ArReversePaymentQueueViewModel_PropertyChanged handling property name {0}", e.PropertyName);
NotificationEvent?.Invoke(this, new NotificationEventArgs("Error in ArReversePaymentQueueViewModel_PropertyChanged", ex.Message));
}
}
Event-Command Bridge
Connecting events to MVVM commands:
// MepApps.Dash.Ar.Maint.PaymentReversal/ViewModels/ArReversePaymentCompletionViewModel.cs (Lines 19-25)
public ArReversePaymentCompletionViewModel(ILogger<ArReversePaymentCompletionViewModel> logger, IArReversePaymentService service)
{
_logger = logger;
_service = service;
ExportToExcelCmd = new RelayCommandAsync<string>(ExportToExcel);
NavigateBackCmd = new RelayCommand(NavigateBack);
}
private void NavigateBack()
{
try
{
NavigateBackEvent?.Invoke(this, new EventArgs());
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in ArReversePaymentCompletionViewModel.NavigateBack");
NotificationEvent?.Invoke(this, new NotificationEventArgs("Error in NavigateBack", ex.Message));
}
}
Cross-Component Communication
Events enable communication between unrelated components:
// Global event aggregator pattern
public class EventAggregator
{
private readonly Dictionary<Type, List<Delegate>> _handlers = new();
public void Subscribe<TEvent>(Action<TEvent> handler) where TEvent : EventArgs
{
if (!_handlers.ContainsKey(typeof(TEvent)))
{
_handlers[typeof(TEvent)] = new List<Delegate>();
}
_handlers[typeof(TEvent)].Add(handler);
}
public void Publish<TEvent>(TEvent eventData) where TEvent : EventArgs
{
if (_handlers.TryGetValue(typeof(TEvent), out var handlers))
{
foreach (Action<TEvent> handler in handlers)
{
handler(eventData);
}
}
}
}
Event Sequencing and Coordination
Managing complex event sequences:
// MepApps.Dash.Ar.Maint.PaymentReversal/ViewModels/ArReversePaymentQueueViewModel.cs (Lines 350-360)
ArReversePaymentPostCompletion completionObject =
await _service.ReversePaymentsAsync(ReversePaymentQueueHeaders, invoices, _selectedPostPeriod.Value);
if (completionObject == null)
{
throw new Exception("CompletionObject is NULL ReversePaymentsAsync");
}
else
{
ClearPaymentsCmd.Execute(null); // First clear the queue
PostCompletedEvent?.Invoke(this, new ReversePaymentPostEventArgs(completionObject)); // Then navigate
}
Error Propagation Through Events
Consistent error handling across event chains:
public class ErrorEventArgs : EventArgs
{
public Exception Exception { get; }
public string Context { get; }
public bool Handled { get; set; }
public ErrorEventArgs(Exception exception, string context)
{
Exception = exception;
Context = context;
Handled = false;
}
}
// Usage
try
{
// Operation
}
catch (Exception ex)
{
var errorArgs = new ErrorEventArgs(ex, "Payment processing");
ErrorOccurred?.Invoke(this, errorArgs);
if (!errorArgs.Handled)
{
// Escalate error
throw;
}
}
Event Testing Strategies
[TestMethod]
public void NavigateToAddPayment_Should_Raise_Event()
{
// Arrange
var viewModel = new ArReversePaymentQueueViewModel(_logger, _service);
bool eventRaised = false;
viewModel.NavigateToAddPayment += (s, e) => eventRaised = true;
// Act
viewModel.AddPaymentCmd.Execute(null);
// Assert
Assert.IsTrue(eventRaised);
}
Best Practices
- Use strongly-typed EventArgs for type safety
- Always unsubscribe events in Dispose methods
- Handle exceptions in event handlers
- Document event contracts clearly
- Use async void carefully only for event handlers
- Avoid long-running operations in event handlers
- Log event flows for debugging
Common Pitfalls
- Memory leaks from unsubscribed events
- Race conditions in async event handlers
- Null reference exceptions from uninitialized events
- Event storms from recursive triggers
- Lost exceptions in async void handlers
Related Documentation
- Navigation Architecture - Overall navigation system
- Page Routing - Route registration and discovery
- Dialog Navigation - Modal dialog patterns
- Navigation Best Practices - Guidelines and tips
Summary
The event-driven navigation system in the AR Payment Reversal dashboard provides a flexible, maintainable approach to managing navigation flows. By leveraging custom EventArgs, proper event lifecycle management, and consistent patterns, the system enables complex navigation scenarios while maintaining clean separation of concerns and testability.