User Interactions
Overview
This document details user interaction patterns in the AR Payment Reversal dashboard, including user input handling, form validation strategies, dialog and popup interactions, keyboard navigation support, accessibility features, user feedback mechanisms, and confirmation/warning patterns. These patterns ensure a responsive, accessible, and user-friendly interface.
Key Concepts
- Input Validation: Real-time and submit-time validation
- Keyboard Support: Full keyboard accessibility
- User Feedback: Visual and auditory feedback
- Accessibility: WCAG compliance features
- Confirmation Patterns: Preventing accidental actions
- Input Masking: Formatted input fields
- Gesture Support: Touch and mouse interactions
Implementation Details
User Input Handling
Input Validation Framework
// MepApps.Dash.Ar.Maint.PaymentReversal/ViewModels/ValidationBase.cs
public abstract class ValidatingViewModel : BaseViewModel, IDataErrorInfo
{
private readonly Dictionary<string, List<string>> _errors = new();
public string this[string propertyName]
{
get
{
ValidateProperty(propertyName);
return _errors.ContainsKey(propertyName)
? string.Join(Environment.NewLine, _errors[propertyName])
: null;
}
}
public string Error => string.Join(Environment.NewLine,
_errors.SelectMany(kvp => kvp.Value));
public bool HasErrors => _errors.Any(kvp => kvp.Value.Any());
protected virtual void ValidateProperty(string propertyName)
{
_errors.Remove(propertyName);
var value = GetType().GetProperty(propertyName)?.GetValue(this);
var errors = new List<string>();
// Get validation attributes
var property = GetType().GetProperty(propertyName);
if (property != null)
{
var validationAttributes = property.GetCustomAttributes<ValidationAttribute>();
foreach (var attribute in validationAttributes)
{
var context = new ValidationContext(this) { MemberName = propertyName };
var result = attribute.GetValidationResult(value, context);
if (result != ValidationResult.Success)
{
errors.Add(result.ErrorMessage);
}
}
}
// Custom validation
var customErrors = ValidatePropertyCustom(propertyName, value);
if (customErrors != null)
{
errors.AddRange(customErrors);
}
if (errors.Any())
{
_errors[propertyName] = errors;
}
OnPropertyChanged(nameof(HasErrors));
OnPropertyChanged($"Item[{propertyName}]");
}
protected virtual IEnumerable<string> ValidatePropertyCustom(string propertyName, object value)
{
return null;
}
public bool ValidateAll()
{
var properties = GetType().GetProperties()
.Where(p => p.CanRead && p.CanWrite);
foreach (var property in properties)
{
ValidateProperty(property.Name);
}
return !HasErrors;
}
}
Input Controls with Validation
<!-- Text input with validation -->
<StackPanel>
<Label Content="Customer Code:" Target="{Binding ElementName=CustomerCodeBox}"/>
<TextBox x:Name="CustomerCodeBox"
Text="{Binding CustomerCode,
ValidatesOnDataErrors=True,
UpdateSourceTrigger=PropertyChanged,
NotifyOnValidationError=True}">
<TextBox.InputBindings>
<KeyBinding Key="Enter" Command="{Binding SearchCommand}"/>
</TextBox.InputBindings>
<i:Interaction.Behaviors>
<local:AutoCompleteBehavior ItemsSource="{Binding CustomerSuggestions}"/>
<local:SelectAllOnFocusBehavior/>
</i:Interaction.Behaviors>
</TextBox>
<!-- Validation message -->
<TextBlock Text="{Binding ElementName=CustomerCodeBox,
Path=(Validation.Errors)[0].ErrorContent}"
Foreground="Red"
FontSize="11"
Visibility="{Binding ElementName=CustomerCodeBox,
Path=(Validation.HasError),
Converter={StaticResource BoolToVisibilityConverter}}"/>
</StackPanel>
<!-- Numeric input with formatting -->
<telerik:RadMaskedNumericInput
Value="{Binding CheckValue, ValidatesOnDataErrors=True}"
Mask="c"
Culture="en-US"
SelectionOnFocus="SelectAll"
SpinMode="Position"
AllowSkipPlaceholders="True"
IsClearButtonVisible="True">
<telerik:RadMaskedNumericInput.InputBindings>
<KeyBinding Key="Up" Command="{Binding IncreaseValueCommand}"/>
<KeyBinding Key="Down" Command="{Binding DecreaseValueCommand}"/>
</telerik:RadMaskedNumericInput.InputBindings>
</telerik:RadMaskedNumericInput>
<!-- Date input with calendar -->
<telerik:RadDatePicker
SelectedDate="{Binding PaymentDate, ValidatesOnDataErrors=True}"
DateSelectionMode="Day"
IsDropDownOpen="{Binding IsCalendarOpen}"
Culture="en-US">
<telerik:RadDatePicker.BlackoutDates>
<telerik:DateRange Start="{Binding FutureDate}" End="12/31/9999"/>
</telerik:RadDatePicker.BlackoutDates>
</telerik:RadDatePicker>
Form Validation Strategies
Validation Rules Implementation
// Custom validation attributes
public class CustomerCodeAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var customerCode = value as string;
if (string.IsNullOrWhiteSpace(customerCode))
{
return new ValidationResult("Customer code is required");
}
if (customerCode.Length > 15)
{
return new ValidationResult("Customer code cannot exceed 15 characters");
}
if (!Regex.IsMatch(customerCode, @"^[A-Z0-9]+$"))
{
return new ValidationResult("Customer code can only contain letters and numbers");
}
// Check if customer exists (async validation)
var service = validationContext.GetService(typeof(IArReversePaymentService))
as IArReversePaymentService;
if (service != null)
{
var customer = Task.Run(async () =>
await service.QueryCustomer(customerCode)).Result;
if (customer == null)
{
return new ValidationResult($"Customer '{customerCode}' not found");
}
}
return ValidationResult.Success;
}
}
// View model with validation
public class PaymentEntryViewModel : ValidatingViewModel
{
private string _customerCode;
[Required(ErrorMessage = "Customer code is required")]
[CustomerCode]
public string CustomerCode
{
get => _customerCode;
set
{
if (SetField(ref _customerCode, value))
{
ValidateProperty(nameof(CustomerCode));
LoadCustomerDetailsCommand.RaiseCanExecuteChanged();
}
}
}
private decimal _checkValue;
[Required(ErrorMessage = "Check value is required")]
[Range(0.01, 9999999.99, ErrorMessage = "Check value must be between 0.01 and 9,999,999.99")]
public decimal CheckValue
{
get => _checkValue;
set
{
if (SetField(ref _checkValue, value))
{
ValidateProperty(nameof(CheckValue));
}
}
}
protected override IEnumerable<string> ValidatePropertyCustom(string propertyName, object value)
{
var errors = new List<string>();
switch (propertyName)
{
case nameof(PaymentDate):
if (value is DateTime date && date > DateTime.Today)
{
errors.Add("Payment date cannot be in the future");
}
break;
case nameof(CheckNumber):
if (value is string checkNumber && IsDuplicateCheckNumber(checkNumber))
{
errors.Add($"Check number '{checkNumber}' already exists");
}
break;
}
return errors;
}
}
Keyboard Navigation Support
Keyboard Handler Implementation
public static class KeyboardNavigationHelper
{
public static void ConfigureKeyboardNavigation(FrameworkElement element)
{
element.PreviewKeyDown += OnPreviewKeyDown;
}
private static void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
var element = sender as FrameworkElement;
if (element == null) return;
// Global shortcuts
if (Keyboard.Modifiers == ModifierKeys.Control)
{
switch (e.Key)
{
case Key.S:
ExecuteCommand(element, "SaveCommand");
e.Handled = true;
break;
case Key.N:
ExecuteCommand(element, "NewCommand");
e.Handled = true;
break;
case Key.F:
FocusSearchBox(element);
e.Handled = true;
break;
case Key.Enter:
ExecuteDefaultAction(element);
e.Handled = true;
break;
}
}
// Navigation shortcuts
switch (e.Key)
{
case Key.F1:
ShowHelp(element);
e.Handled = true;
break;
case Key.Escape:
CancelCurrentOperation(element);
e.Handled = true;
break;
case Key.Tab:
if (Keyboard.Modifiers == ModifierKeys.Shift)
{
MoveFocusPrevious(element);
}
else
{
MoveFocusNext(element);
}
e.Handled = true;
break;
}
}
private static void MoveFocusNext(FrameworkElement element)
{
var request = new TraversalRequest(FocusNavigationDirection.Next);
element.MoveFocus(request);
}
private static void MoveFocusPrevious(FrameworkElement element)
{
var request = new TraversalRequest(FocusNavigationDirection.Previous);
element.MoveFocus(request);
}
}
Tab Order Configuration
<!-- Explicit tab order -->
<Grid>
<Grid.Resources>
<Style TargetType="Control">
<Setter Property="IsTabStop" Value="True"/>
</Style>
</Grid.Resources>
<TextBox TabIndex="1" x:Name="CustomerCode"/>
<Button TabIndex="2" Content="Search" IsDefault="True"/>
<TextBox TabIndex="3" x:Name="CheckNumber"/>
<TextBox TabIndex="4" x:Name="CheckValue"/>
<DatePicker TabIndex="5" x:Name="PaymentDate"/>
<ComboBox TabIndex="6" x:Name="BankAccount"/>
<Button TabIndex="7" Content="Add to Queue"/>
<Button TabIndex="8" Content="Cancel" IsCancel="True"/>
</Grid>
Accessibility Features
Screen Reader Support
<!-- Accessibility properties -->
<Button x:Name="AddPaymentButton"
Content="Add Payment"
AutomationProperties.Name="Add new payment to reversal queue"
AutomationProperties.HelpText="Click to add a new payment reversal to the processing queue"
AutomationProperties.AcceleratorKey="Ctrl+A"
AutomationProperties.AutomationId="AddPaymentButton">
<Button.ToolTip>
<ToolTip>
<StackPanel>
<TextBlock FontWeight="Bold">Add Payment (Ctrl+A)</TextBlock>
<TextBlock>Add a new payment to the reversal queue</TextBlock>
</StackPanel>
</ToolTip>
</Button.ToolTip>
</Button>
<!-- Accessible data grid -->
<telerik:RadGridView AutomationProperties.Name="Payment Queue Grid"
AutomationProperties.LiveSetting="Polite">
<telerik:RadGridView.RowStyle>
<Style TargetType="telerik:GridViewRow">
<Setter Property="AutomationProperties.Name">
<Setter.Value>
<MultiBinding StringFormat="Payment from {0} for {1}">
<Binding Path="Customer"/>
<Binding Path="CheckValue" Converter="{StaticResource CurrencyConverter}"/>
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</telerik:RadGridView.RowStyle>
</telerik:RadGridView>
High Contrast Support
public class AccessibilityManager
{
public static void ApplyHighContrastTheme(FrameworkElement rootElement)
{
if (SystemParameters.HighContrast)
{
var highContrastResources = new ResourceDictionary
{
Source = new Uri("/Themes/HighContrast.xaml", UriKind.Relative)
};
rootElement.Resources.MergedDictionaries.Add(highContrastResources);
}
}
public static void ConfigureAccessibility(UIElement element)
{
// Set focus visual style
element.FocusVisualStyle = Application.Current.Resources["AccessibleFocusVisual"] as Style;
// Enable keyboard navigation
KeyboardNavigation.SetTabNavigation(element, KeyboardNavigationMode.Cycle);
KeyboardNavigation.SetDirectionalNavigation(element, KeyboardNavigationMode.Continue);
// Configure automation properties
if (element is FrameworkElement fe && string.IsNullOrEmpty(AutomationProperties.GetName(fe)))
{
// Auto-generate automation name from content or name
if (fe is ContentControl cc && cc.Content is string content)
{
AutomationProperties.SetName(fe, content);
}
else if (!string.IsNullOrEmpty(fe.Name))
{
AutomationProperties.SetName(fe, fe.Name.Replace("_", " "));
}
}
}
}
User Feedback Mechanisms
Visual Feedback
public class VisualFeedbackService
{
public static void ShowProcessingFeedback(FrameworkElement element, string message)
{
// Create overlay
var overlay = new Grid
{
Background = new SolidColorBrush(Color.FromArgb(128, 0, 0, 0))
};
var messagePanel = new Border
{
Background = Brushes.White,
CornerRadius = new CornerRadius(5),
Padding = new Thickness(20),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Child = new StackPanel
{
Children =
{
new ProgressBar { IsIndeterminate = true, Width = 200, Height = 20 },
new TextBlock { Text = message, Margin = new Thickness(0, 10, 0, 0) }
}
}
};
overlay.Children.Add(messagePanel);
// Add to visual tree
if (element is Panel panel)
{
panel.Children.Add(overlay);
Panel.SetZIndex(overlay, 9999);
}
}
public static void FlashElement(FrameworkElement element, Color color)
{
var originalBackground = element.GetValue(Control.BackgroundProperty);
var colorAnimation = new ColorAnimation
{
From = color,
To = Colors.Transparent,
Duration = TimeSpan.FromSeconds(0.5),
AutoReverse = true,
RepeatBehavior = new RepeatBehavior(2)
};
var brush = new SolidColorBrush();
element.SetValue(Control.BackgroundProperty, brush);
colorAnimation.Completed += (s, e) =>
{
element.SetValue(Control.BackgroundProperty, originalBackground);
};
brush.BeginAnimation(SolidColorBrush.ColorProperty, colorAnimation);
}
}
Confirmation and Warning Patterns
Confirmation Dialog Service
public class ConfirmationService
{
public enum ConfirmationResult
{
Yes,
No,
Cancel
}
public async Task<ConfirmationResult> ConfirmAsync(
string message,
string title = "Confirm",
ConfirmationButtons buttons = ConfirmationButtons.YesNo)
{
var dialog = new ConfirmationDialog
{
Title = title,
Message = message,
Buttons = buttons
};
var result = await ShowDialogAsync(dialog);
return result;
}
public async Task<bool> ConfirmDestructiveAction(
string itemDescription,
string actionDescription = "delete")
{
var message = $"Are you sure you want to {actionDescription} {itemDescription}?\n\n" +
"This action cannot be undone.";
var dialog = new ConfirmationDialog
{
Title = "Confirm Action",
Message = message,
Buttons = ConfirmationButtons.YesNo,
Icon = MessageBoxImage.Warning,
DefaultButton = ConfirmationResult.No,
ShowDontAskAgain = true
};
// Highlight destructive action
dialog.YesButton.Background = Brushes.Red;
dialog.YesButton.Foreground = Brushes.White;
var result = await ShowDialogAsync(dialog);
return result == ConfirmationResult.Yes;
}
public async Task<bool> WarnDataLoss()
{
var message = "You have unsaved changes. Do you want to save before continuing?";
var result = await ConfirmAsync(
message,
"Unsaved Changes",
ConfirmationButtons.YesNoCancel);
switch (result)
{
case ConfirmationResult.Yes:
// Save and continue
return await SaveDataAsync();
case ConfirmationResult.No:
// Discard and continue
return true;
case ConfirmationResult.Cancel:
// Stay on current screen
return false;
}
return false;
}
}
Inline Warning Display
<!-- Warning banner -->
<Border x:Name="WarningBanner"
Background="LightYellow"
BorderBrush="Orange"
BorderThickness="1"
Padding="10"
Margin="5"
Visibility="{Binding HasWarnings, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Image Source="/Images/warning.png" Width="24" Height="24" Margin="0,0,10,0"/>
<TextBlock Grid.Column="1"
Text="{Binding WarningMessage}"
VerticalAlignment="Center"
TextWrapping="Wrap"/>
<Button Grid.Column="2"
Content="Dismiss"
Command="{Binding DismissWarningCommand}"
Margin="10,0,0,0"/>
</Grid>
</Border>
Drag and Drop Support
public static class DragDropHelper
{
public static void EnableDragDrop(ItemsControl itemsControl)
{
itemsControl.PreviewMouseLeftButtonDown += OnPreviewMouseLeftButtonDown;
itemsControl.PreviewMouseMove += OnPreviewMouseMove;
itemsControl.Drop += OnDrop;
itemsControl.AllowDrop = true;
}
private static void OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var itemsControl = sender as ItemsControl;
var item = GetItemUnderMouse(itemsControl, e.GetPosition(itemsControl));
if (item != null)
{
_draggedItem = item;
_startPoint = e.GetPosition(null);
}
}
private static void OnPreviewMouseMove(object sender, MouseEventArgs e)
{
if (_draggedItem == null || e.LeftButton != MouseButtonState.Pressed)
return;
var position = e.GetPosition(null);
var diff = _startPoint - position;
if (Math.Abs(diff.X) > SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(diff.Y) > SystemParameters.MinimumVerticalDragDistance)
{
var data = new DataObject("PaymentItem", _draggedItem);
DragDrop.DoDragDrop((DependencyObject)sender, data, DragDropEffects.Move);
_draggedItem = null;
}
}
private static void OnDrop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent("PaymentItem"))
{
var droppedItem = e.Data.GetData("PaymentItem");
var targetItem = GetItemUnderMouse(sender as ItemsControl, e.GetPosition(sender as IInputElement));
// Reorder items
if (droppedItem != null && targetItem != null)
{
ReorderItems(droppedItem, targetItem);
}
}
}
}
Best Practices
- Provide immediate feedback for user actions
- Validate input as users type when possible
- Support full keyboard navigation throughout the app
- Include accessibility properties for screen readers
- Use clear confirmation dialogs for destructive actions
- Show progress for long-running operations
- Handle errors gracefully with helpful messages
Common Pitfalls
- Missing keyboard support for mouse-only interactions
- No feedback during async operations
- Unclear validation messages that don't help users
- Inaccessible custom controls for screen readers
- No confirmation for destructive actions
Related Documentation
- UI Architecture - Overall UI design
- UI Components - Reusable controls
- Data Presentation - Data display
- MVVM Patterns - Data binding
Summary
The user interaction patterns in the AR Payment Reversal dashboard ensure a responsive, accessible, and user-friendly experience. Through comprehensive input validation, keyboard support, accessibility features, and clear feedback mechanisms, the application provides an intuitive interface that accommodates users of all abilities while preventing errors and data loss.