Dialog Navigation
Overview
Modal dialog navigation in the AR Payment Reversal dashboard implements sophisticated patterns for managing dialog lifecycles, data exchange, and user interactions. This document details how dialogs are opened, configured, closed, and how data flows between parent views and modal dialogs while maintaining clean separation of concerns.
Key Concepts
- Modal Dialog Management: Centralized dialog service with lifecycle control
- Data Contract Pattern: Strongly-typed data exchange between dialogs and parents
- Dialog Result Handling: Structured approach to capturing user decisions
- Dialog Stacking: Support for nested modal scenarios
- Validation Integration: Built-in validation before dialog closure
Implementation Details
Dialog Infrastructure
Base Dialog ViewModel Pattern
// MepApps.Dash.Ar.Maint.PaymentReversal/ViewModels/Dialog/BaseDialogViewModel.cs
public abstract class BaseDialogViewModel : BaseViewModel, IDialogViewModel
{
public event EventHandler<DialogResultEventArgs> CloseRequested;
protected bool? _dialogResult;
public bool? DialogResult
{
get => _dialogResult;
protected set
{
if (_dialogResult != value)
{
_dialogResult = value;
OnPropertyChanged();
CloseRequested?.Invoke(this, new DialogResultEventArgs(value));
}
}
}
public RelayCommand ConfirmCommand { get; }
public RelayCommand CancelCommand { get; }
protected BaseDialogViewModel()
{
ConfirmCommand = new RelayCommand(OnConfirm, CanConfirm);
CancelCommand = new RelayCommand(OnCancel);
}
protected virtual void OnConfirm()
{
if (Validate())
{
DialogResult = true;
}
}
protected virtual void OnCancel()
{
DialogResult = false;
}
protected virtual bool CanConfirm() => true;
protected virtual bool Validate() => true;
}
Dialog Types and Implementations
1. Customer Selection Dialog
Complex selection dialog with search and filtering:
// MepApps.Dash.Ar.Maint.PaymentReversal/ViewModels/Dialog/CustomerSelectionDialogViewModel.cs
public class CustomerSelectionDialogViewModel : BaseDialogViewModel
{
private readonly IArReversePaymentService _service;
private readonly ILogger<CustomerSelectionDialogViewModel> _logger;
public CustomerSelectionDialogViewModel(
ILogger<CustomerSelectionDialogViewModel> logger,
IArReversePaymentService service)
{
_logger = logger;
_service = service;
SearchCommand = new RelayCommandAsync(SearchCustomers);
SelectCommand = new RelayCommand<CustomerItem>(SelectCustomer);
}
private ObservableCollection<CustomerItem> _customers;
public ObservableCollection<CustomerItem> Customers
{
get => _customers;
set
{
if (_customers != value)
{
_customers = value;
OnPropertyChanged();
}
}
}
private CustomerItem _selectedCustomer;
public CustomerItem SelectedCustomer
{
get => _selectedCustomer;
set
{
if (_selectedCustomer != value)
{
_selectedCustomer = value;
OnPropertyChanged();
ConfirmCommand.RaiseCanExecuteChanged();
}
}
}
protected override bool CanConfirm() => SelectedCustomer != null;
protected override bool Validate()
{
if (SelectedCustomer == null)
{
MessageBox.Show("Please select a customer.", "Validation Error");
return false;
}
return true;
}
private void SelectCustomer(CustomerItem customer)
{
SelectedCustomer = customer;
OnConfirm();
}
}
2. Settings Dialog
Configuration dialog with validation:
// MepApps.Dash.Ar.Maint.PaymentReversal/ViewModels/Dialog/SettingsDialogViewModel.cs
public class SettingsDialogViewModel : BaseDialogViewModel
{
private readonly IMepSettingsService _settingsService;
private MepSettings _settings;
private MepSettings _originalSettings;
public SettingsDialogViewModel(IMepSettingsService settingsService)
{
_settingsService = settingsService;
LoadSettings();
}
private void LoadSettings()
{
_settings = _settingsService.GetSettings();
_originalSettings = _settings.Clone(); // Keep original for cancel
}
public string DefaultBank
{
get => _settings.DefaultBank;
set
{
if (_settings.DefaultBank != value)
{
_settings.DefaultBank = value;
OnPropertyChanged();
IsDirty = true;
}
}
}
private bool _isDirty;
public bool IsDirty
{
get => _isDirty;
private set
{
if (_isDirty != value)
{
_isDirty = value;
OnPropertyChanged();
}
}
}
protected override void OnConfirm()
{
if (Validate())
{
_settingsService.SaveSettings(_settings);
DialogResult = true;
}
}
protected override void OnCancel()
{
if (IsDirty)
{
var result = MessageBox.Show(
"You have unsaved changes. Are you sure you want to cancel?",
"Unsaved Changes",
MessageBoxButton.YesNo);
if (result == MessageBoxResult.No)
return;
}
DialogResult = false;
}
}
Dialog Service Implementation
Centralized service for managing dialog lifecycle:
// MepApps.Dash.Ar.Maint.PaymentReversal/Services/DialogService.cs
public interface IDialogService
{
Task<T> ShowDialogAsync<T>(IDialogViewModel viewModel) where T : class;
Task<bool?> ShowDialogAsync(IDialogViewModel viewModel);
void CloseDialog(IDialogViewModel viewModel);
}
public class DialogService : IDialogService
{
private readonly IViewFactoryService _viewFactory;
private readonly Dictionary<IDialogViewModel, Window> _openDialogs = new();
public DialogService(IViewFactoryService viewFactory)
{
_viewFactory = viewFactory;
}
public async Task<T> ShowDialogAsync<T>(IDialogViewModel viewModel) where T : class
{
var tcs = new TaskCompletionSource<T>();
Application.Current.Dispatcher.Invoke(() =>
{
var dialogView = CreateDialogView(viewModel);
var window = new Window
{
Content = dialogView,
Owner = Application.Current.MainWindow,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
ShowInTaskbar = false,
ResizeMode = ResizeMode.NoResize,
WindowStyle = WindowStyle.ToolWindow
};
_openDialogs[viewModel] = window;
viewModel.CloseRequested += (s, e) =>
{
window.DialogResult = e.DialogResult;
window.Close();
};
window.Closed += (s, e) =>
{
_openDialogs.Remove(viewModel);
tcs.SetResult(viewModel as T);
};
window.ShowDialog();
});
return await tcs.Task;
}
public void CloseDialog(IDialogViewModel viewModel)
{
if (_openDialogs.TryGetValue(viewModel, out var window))
{
Application.Current.Dispatcher.Invoke(() => window.Close());
}
}
}
Data Passing to Dialogs
Input Data Pattern
// MepApps.Dash.Ar.Maint.PaymentReversal/ViewModels/Dialog/CustomFormValidationDialogViewModel.cs
public class CustomFormValidationDialogViewModel : BaseDialogViewModel
{
private CustomFormField _formField;
public void Initialize(CustomFormField formField)
{
_formField = formField;
LoadValidationRules();
}
private void LoadValidationRules()
{
// Load validation rules based on form field
ValidationRules = _customFormService.GetValidationRules(_formField);
}
}
// Usage
var dialogViewModel = _serviceProvider.GetService<CustomFormValidationDialogViewModel>();
dialogViewModel.Initialize(selectedFormField);
var result = await _dialogService.ShowDialogAsync(dialogViewModel);
Output Data Pattern
// MepApps.Dash.Ar.Maint.PaymentReversal/ViewModels/Dialog/DatabaseValidationDialogViewModel.cs
public class DatabaseValidationDialogViewModel : BaseDialogViewModel
{
public DatabaseValidationObject ValidationResult { get; private set; }
protected override void OnConfirm()
{
if (Validate())
{
ValidationResult = new DatabaseValidationObject
{
TableName = SelectedTable,
ValidationPassed = true,
ValidationDate = DateTime.Now
};
DialogResult = true;
}
}
}
// Usage
var dialogViewModel = _serviceProvider.GetService<DatabaseValidationDialogViewModel>();
var result = await _dialogService.ShowDialogAsync<DatabaseValidationDialogViewModel>(dialogViewModel);
if (result?.DialogResult == true)
{
var validationResult = result.ValidationResult;
// Process validation result
}
Dialog Result Handling
Structured Result Pattern
public class DialogResult<T>
{
public bool Confirmed { get; set; }
public T Data { get; set; }
public string CancelReason { get; set; }
public static DialogResult<T> Ok(T data) => new DialogResult<T>
{
Confirmed = true,
Data = data
};
public static DialogResult<T> Cancel(string reason = null) => new DialogResult<T>
{
Confirmed = false,
CancelReason = reason
};
}
// Usage in ViewModel
public async Task<DialogResult<CustomerItem>> SelectCustomerAsync()
{
var dialog = new CustomerSelectionDialogViewModel(_logger, _service);
var result = await _dialogService.ShowDialogAsync(dialog);
if (result?.DialogResult == true)
{
return DialogResult<CustomerItem>.Ok(dialog.SelectedCustomer);
}
return DialogResult<CustomerItem>.Cancel("User cancelled selection");
}
Dialog Stacking and Chaining
Managing multiple dialog levels:
public class DialogStackManager
{
private readonly Stack<IDialogViewModel> _dialogStack = new();
private readonly IDialogService _dialogService;
public async Task<T> ShowStackedDialogAsync<T>(IDialogViewModel dialog) where T : class
{
_dialogStack.Push(dialog);
try
{
return await _dialogService.ShowDialogAsync<T>(dialog);
}
finally
{
_dialogStack.Pop();
}
}
public bool HasOpenDialogs => _dialogStack.Count > 0;
public void CloseAll()
{
while (_dialogStack.Count > 0)
{
var dialog = _dialogStack.Pop();
_dialogService.CloseDialog(dialog);
}
}
}
Dialog Animation and Transitions
<!-- MepApps.Dash.Ar.Maint.PaymentReversal/Views/Dialog/CustomerSelectionDialogView.xaml -->
<UserControl.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
From="0" To="1" Duration="0:0:0.3"/>
<DoubleAnimation
Storyboard.TargetName="DialogContent"
Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleX)"
From="0.8" To="1" Duration="0:0:0.2">
<DoubleAnimation.EasingFunction>
<BackEase EasingMode="EaseOut" Amplitude="0.3"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</UserControl.Triggers>
Modal Dialog Validation
Preventing closure on validation failure:
// MepApps.Dash.Ar.Maint.PaymentReversal/ViewModels/Dialog/MepSettingsViewModel.cs
public class MepSettingsViewModel : BaseDialogViewModel
{
protected override bool Validate()
{
var errors = new List<string>();
if (string.IsNullOrWhiteSpace(DefaultBank))
{
errors.Add("Default bank is required");
}
if (PostingBatchSize < 1 || PostingBatchSize > 1000)
{
errors.Add("Batch size must be between 1 and 1000");
}
if (errors.Any())
{
MessageBox.Show(
string.Join("\n", errors),
"Validation Errors",
MessageBoxButton.OK,
MessageBoxImage.Warning);
return false;
}
return true;
}
}
Dialog Memory Management
Proper cleanup when dialogs close:
public class DialogViewModelBase : BaseDialogViewModel, IDisposable
{
private bool _disposed;
protected override void OnCancel()
{
Cleanup();
base.OnCancel();
}
protected override void OnConfirm()
{
if (Validate())
{
SaveData();
Cleanup();
base.OnConfirm();
}
}
private void Cleanup()
{
// Unsubscribe events
// Clear collections
// Dispose resources
}
public void Dispose()
{
if (!_disposed)
{
Cleanup();
_disposed = true;
}
}
}
Testing Dialog Navigation
[TestClass]
public class DialogNavigationTests
{
[TestMethod]
public async Task CustomerSelectionDialog_Should_Return_Selected_Customer()
{
// Arrange
var dialogService = new MockDialogService();
var viewModel = new CustomerSelectionDialogViewModel(_logger, _service);
var expectedCustomer = new CustomerItem { Customer = "C001", Name = "Test" };
// Act
viewModel.SelectedCustomer = expectedCustomer;
viewModel.ConfirmCommand.Execute(null);
// Assert
Assert.IsTrue(viewModel.DialogResult);
Assert.AreEqual(expectedCustomer, viewModel.SelectedCustomer);
}
}
Best Practices
- Always validate input before closing dialogs
- Use view models for dialog logic, not code-behind
- Handle escape key for cancel action
- Provide clear titles and instructions
- Implement proper disposal for resource cleanup
- Use async patterns for non-blocking dialogs
- Test dialog flows including cancel scenarios
Common Pitfalls
- Memory leaks from unclosed dialogs
- Blocking UI thread with synchronous operations
- Missing validation allowing invalid data
- Poor error handling in dialog operations
- Circular dependencies between dialogs
Related Documentation
- Navigation Architecture - Overall navigation system
- Page Routing - Route registration
- Navigation Events - Event-driven navigation
- Navigation Best Practices - Guidelines
Summary
The dialog navigation system in the AR Payment Reversal dashboard provides a robust framework for modal interactions. Through proper use of view models, data contracts, and lifecycle management, the system ensures consistent, testable, and user-friendly dialog experiences while maintaining clean separation of concerns.