Skip to main content

UI Components

Overview

This document details the reusable UI components in the AR Payment Reversal dashboard, including custom controls implementation, data templates and styles, converter usage, ResourceDictionary organization, component composition patterns, and examples of complex UI scenarios. These components form the building blocks for creating consistent, maintainable user interfaces throughout the application.

Key Concepts

  • Custom Controls: Specialized WPF controls for specific needs
  • Data Templates: Visual representation of data objects
  • Value Converters: Data transformation for display
  • Control Templates: Custom control appearance
  • User Controls: Composite reusable components
  • Attached Properties: Extending existing controls
  • Behaviors: Reusable interaction logic

Implementation Details

Custom Controls

Customer Selector Control

// MepApps.Dash.Ar.Maint.PaymentReversal/Controls/CustomerSelector.cs
public class CustomerSelector : Control
{
static CustomerSelector()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(CustomerSelector),
new FrameworkPropertyMetadata(typeof(CustomerSelector)));
}

// Dependency Properties
public static readonly DependencyProperty SelectedCustomerProperty =
DependencyProperty.Register(
nameof(SelectedCustomer),
typeof(string),
typeof(CustomerSelector),
new FrameworkPropertyMetadata(
null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnSelectedCustomerChanged));

public string SelectedCustomer
{
get => (string)GetValue(SelectedCustomerProperty);
set => SetValue(SelectedCustomerProperty, value);
}

public static readonly DependencyProperty CustomerNameProperty =
DependencyProperty.Register(
nameof(CustomerName),
typeof(string),
typeof(CustomerSelector),
new PropertyMetadata(string.Empty));

public string CustomerName
{
get => (string)GetValue(CustomerNameProperty);
set => SetValue(CustomerNameProperty, value);
}

// Routed Events
public static readonly RoutedEvent CustomerSelectedEvent =
EventManager.RegisterRoutedEvent(
nameof(CustomerSelected),
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(CustomerSelector));

public event RoutedEventHandler CustomerSelected
{
add { AddHandler(CustomerSelectedEvent, value); }
remove { RemoveHandler(CustomerSelectedEvent, value); }
}

// Commands
public static readonly DependencyProperty SelectCommandProperty =
DependencyProperty.Register(
nameof(SelectCommand),
typeof(ICommand),
typeof(CustomerSelector));

public ICommand SelectCommand
{
get => (ICommand)GetValue(SelectCommandProperty);
set => SetValue(SelectCommandProperty, value);
}

public override void OnApplyTemplate()
{
base.OnApplyTemplate();

// Get template parts
var textBox = GetTemplateChild("PART_TextBox") as TextBox;
var button = GetTemplateChild("PART_SelectButton") as Button;

if (button != null)
{
button.Click += OnSelectButtonClick;
}

if (textBox != null)
{
textBox.TextChanged += OnTextBoxTextChanged;
}
}

private void OnSelectButtonClick(object sender, RoutedEventArgs e)
{
// Open customer selection dialog
var dialog = new CustomerSelectionDialog();
if (dialog.ShowDialog() == true)
{
SelectedCustomer = dialog.SelectedCustomer.Customer;
CustomerName = dialog.SelectedCustomer.Name;

// Raise event
RaiseEvent(new RoutedEventArgs(CustomerSelectedEvent));
}
}
}

Customer Selector Template

<!-- Generic.xaml or ResourceDictionary -->
<Style TargetType="{x:Type local:CustomerSelector}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CustomerSelector}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>

<TextBox x:Name="PART_TextBox"
Grid.Column="0"
Text="{Binding RelativeSource={RelativeSource TemplatedParent},
Path=SelectedCustomer, Mode=TwoWay}"
VerticalAlignment="Center"/>

<TextBlock Grid.Column="1"
Text="{Binding RelativeSource={RelativeSource TemplatedParent},
Path=CustomerName}"
VerticalAlignment="Center"
Margin="5,0"/>

<Button x:Name="PART_SelectButton"
Grid.Column="2"
Content="..."
Width="30"
Command="{Binding RelativeSource={RelativeSource TemplatedParent},
Path=SelectCommand}"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

Data Templates

Payment Header Template

<!-- MepApps.Dash.Ar.Maint.PaymentReversal/Resources/DataTemplates.xaml -->
<DataTemplate x:Key="PaymentHeaderTemplate" DataType="{x:Type model:ArReversePaymentHeader}">
<Border BorderBrush="LightGray" BorderThickness="1"
CornerRadius="3" Margin="2" Padding="5">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>

<!-- Selection checkbox -->
<CheckBox Grid.Column="0"
IsChecked="{Binding Selected}"
IsEnabled="{Binding SelectEnabled}"
VerticalAlignment="Center"/>

<!-- Payment details -->
<StackPanel Grid.Column="1" Margin="10,0">
<TextBlock FontWeight="Bold">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} - Check #{1}">
<Binding Path="Customer"/>
<Binding Path="CheckNumber"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>

<TextBlock Text="{Binding CustomerName}"
Foreground="Gray" FontSize="11"/>

<StackPanel Orientation="Horizontal">
<TextBlock Text="Date: " FontSize="11"/>
<TextBlock Text="{Binding PaymentDate, StringFormat=d}"
FontSize="11"/>
<TextBlock Text=" | Bank: " FontSize="11" Margin="10,0,0,0"/>
<TextBlock Text="{Binding BankDescription}" FontSize="11"/>
</StackPanel>
</StackPanel>

<!-- Amount -->
<TextBlock Grid.Column="2"
Text="{Binding CheckValue, StringFormat=C}"
FontWeight="Bold"
FontSize="14"
VerticalAlignment="Center"/>
</Grid>
</Border>
</DataTemplate>

<!-- Hierarchical template for grouped data -->
<HierarchicalDataTemplate x:Key="GroupedPaymentTemplate"
DataType="{x:Type model:PaymentGroup}"
ItemsSource="{Binding Payments}">
<Border Background="#F0F0F0" Padding="5">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding GroupName}" FontWeight="Bold"/>
<TextBlock Text=" (" Margin="5,0,0,0"/>
<TextBlock Text="{Binding PaymentCount}"/>
<TextBlock Text=" payments, "/>
<TextBlock Text="{Binding TotalAmount, StringFormat=C}"/>
<TextBlock Text=")"/>
</StackPanel>
</Border>
</HierarchicalDataTemplate>

Value Converters

NullEmptyIEnumerableToBooleanConverter

// MepApps.Dash.Ar.Maint.PaymentReversal/Resources/Converters/NullEmptyIEnumerableToBooleanConverter.cs
public class NullEmptyIEnumerableToBooleanConverter : IValueConverter
{
public bool EmptyValue { get; set; } = false;
public bool NonEmptyValue { get; set; } = true;

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
return EmptyValue;

if (value is IEnumerable enumerable)
{
// Check if enumerable has any items
var enumerator = enumerable.GetEnumerator();
var hasItems = enumerator.MoveNext();

// Dispose if necessary
if (enumerator is IDisposable disposable)
disposable.Dispose();

return hasItems ? NonEmptyValue : EmptyValue;
}

return EmptyValue;
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

// Multi-value converter example
public class PaymentStatusToColorConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length < 2)
return Brushes.Gray;

var status = values[0] as string;
var hasErrors = values[1] as bool? ?? false;

if (hasErrors)
return Brushes.Red;

return status switch
{
"Posted" => Brushes.Green,
"Pending" => Brushes.Orange,
"Processing" => Brushes.Blue,
"Failed" => Brushes.Red,
_ => Brushes.Gray
};
}

public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

User Controls

Payment Summary Control

<!-- MepApps.Dash.Ar.Maint.PaymentReversal/Controls/PaymentSummaryControl.xaml -->
<UserControl x:Class="MepApps.Dash.Ar.Maint.PaymentReversal.Controls.PaymentSummaryControl">
<UserControl.Resources>
<local:CurrencyFormatConverter x:Key="CurrencyConverter"/>
<local:PaymentStatusToColorConverter x:Key="StatusColorConverter"/>
</UserControl.Resources>

<Border BorderBrush="Black" BorderThickness="1" CornerRadius="5" Padding="10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>

<!-- Header -->
<TextBlock Grid.Row="0" Text="Payment Summary"
FontSize="16" FontWeight="Bold" Margin="0,0,0,10"/>

<!-- Status indicator -->
<Border Grid.Row="1" Height="5" Margin="0,0,0,10">
<Border.Background>
<MultiBinding Converter="{StaticResource StatusColorConverter}">
<Binding Path="Status"/>
<Binding Path="HasErrors"/>
</MultiBinding>
</Border.Background>
</Border>

<!-- Details -->
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>

<TextBlock Grid.Row="0" Grid.Column="0" Text="Payments:" Margin="0,2"/>
<TextBlock Grid.Row="0" Grid.Column="1"
Text="{Binding PaymentCount}"
FontWeight="Bold" Margin="10,2"/>

<TextBlock Grid.Row="1" Grid.Column="0" Text="Total Amount:" Margin="0,2"/>
<TextBlock Grid.Row="1" Grid.Column="1"
Text="{Binding TotalAmount, Converter={StaticResource CurrencyConverter}}"
FontWeight="Bold" Margin="10,2"/>

<TextBlock Grid.Row="2" Grid.Column="0" Text="Status:" Margin="0,2"/>
<TextBlock Grid.Row="2" Grid.Column="1"
Text="{Binding Status}"
FontWeight="Bold" Margin="10,2"/>

<TextBlock Grid.Row="3" Grid.Column="0" Text="Period:" Margin="0,2"/>
<TextBlock Grid.Row="3" Grid.Column="1"
Text="{Binding PostingPeriod}"
Margin="10,2"/>
</Grid>

<!-- Actions -->
<StackPanel Grid.Row="3" Orientation="Horizontal"
HorizontalAlignment="Right" Margin="0,10,0,0">
<Button Content="View Details" Command="{Binding ViewDetailsCommand}"
Margin="0,0,5,0"/>
<Button Content="Export" Command="{Binding ExportCommand}"/>
</StackPanel>
</Grid>
</Border>
</UserControl>

Attached Properties

Validation Attached Properties

public static class ValidationAttached
{
// Show validation tooltip
public static readonly DependencyProperty ShowValidationTooltipProperty =
DependencyProperty.RegisterAttached(
"ShowValidationTooltip",
typeof(bool),
typeof(ValidationAttached),
new PropertyMetadata(false, OnShowValidationTooltipChanged));

public static bool GetShowValidationTooltip(DependencyObject obj)
{
return (bool)obj.GetValue(ShowValidationTooltipProperty);
}

public static void SetShowValidationTooltip(DependencyObject obj, bool value)
{
obj.SetValue(ShowValidationTooltipProperty, value);
}

private static void OnShowValidationTooltipChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is FrameworkElement element)
{
if ((bool)e.NewValue)
{
element.Loaded += Element_Loaded;
}
else
{
element.Loaded -= Element_Loaded;
}
}
}

private static void Element_Loaded(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement element)
{
var binding = new Binding
{
Source = element,
Path = new PropertyPath("(Validation.Errors)[0].ErrorContent")
};

element.SetBinding(ToolTipService.ToolTipProperty, binding);
}
}

// Watermark text
public static readonly DependencyProperty WatermarkProperty =
DependencyProperty.RegisterAttached(
"Watermark",
typeof(string),
typeof(ValidationAttached),
new PropertyMetadata(string.Empty, OnWatermarkChanged));

public static string GetWatermark(DependencyObject obj)
{
return (string)obj.GetValue(WatermarkProperty);
}

public static void SetWatermark(DependencyObject obj, string value)
{
obj.SetValue(WatermarkProperty, value);
}

private static void OnWatermarkChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TextBox textBox)
{
textBox.GotFocus += RemoveWatermark;
textBox.LostFocus += ShowWatermark;

if (string.IsNullOrEmpty(textBox.Text))
{
ShowWatermark(textBox, null);
}
}
}

private static void ShowWatermark(object sender, RoutedEventArgs e)
{
if (sender is TextBox textBox && string.IsNullOrEmpty(textBox.Text))
{
textBox.Text = GetWatermark(textBox);
textBox.Foreground = Brushes.Gray;
}
}

private static void RemoveWatermark(object sender, RoutedEventArgs e)
{
if (sender is TextBox textBox && textBox.Text == GetWatermark(textBox))
{
textBox.Text = string.Empty;
textBox.Foreground = Brushes.Black;
}
}
}

Behaviors

Auto-Complete Behavior

public class AutoCompleteBehavior : Behavior<TextBox>
{
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register(
nameof(ItemsSource),
typeof(IEnumerable),
typeof(AutoCompleteBehavior));

public IEnumerable ItemsSource
{
get => (IEnumerable)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}

private Popup _popup;
private ListBox _listBox;

protected override void OnAttached()
{
base.OnAttached();

CreatePopup();

AssociatedObject.TextChanged += OnTextChanged;
AssociatedObject.PreviewKeyDown += OnPreviewKeyDown;
AssociatedObject.LostFocus += OnLostFocus;
}

private void CreatePopup()
{
_popup = new Popup
{
PlacementTarget = AssociatedObject,
Placement = PlacementMode.Bottom,
Width = AssociatedObject.ActualWidth
};

_listBox = new ListBox
{
MaxHeight = 200
};

_listBox.PreviewMouseDown += OnListBoxItemSelected;

var border = new Border
{
BorderBrush = Brushes.Gray,
BorderThickness = new Thickness(1),
Child = _listBox
};

_popup.Child = border;
}

private void OnTextChanged(object sender, TextChangedEventArgs e)
{
if (string.IsNullOrEmpty(AssociatedObject.Text))
{
_popup.IsOpen = false;
return;
}

var filteredItems = FilterItems(AssociatedObject.Text);

if (filteredItems.Any())
{
_listBox.ItemsSource = filteredItems;
_popup.IsOpen = true;
}
else
{
_popup.IsOpen = false;
}
}

private IEnumerable FilterItems(string searchText)
{
if (ItemsSource == null)
return Enumerable.Empty<object>();

return ItemsSource.Cast<object>()
.Where(item => item.ToString().StartsWith(searchText, StringComparison.OrdinalIgnoreCase));
}

private void OnListBoxItemSelected(object sender, MouseButtonEventArgs e)
{
if (_listBox.SelectedItem != null)
{
AssociatedObject.Text = _listBox.SelectedItem.ToString();
_popup.IsOpen = false;
}
}

protected override void OnDetaching()
{
AssociatedObject.TextChanged -= OnTextChanged;
AssociatedObject.PreviewKeyDown -= OnPreviewKeyDown;
AssociatedObject.LostFocus -= OnLostFocus;

base.OnDetaching();
}
}

Complex UI Scenarios

Master-Detail View

<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" MinWidth="200"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="*" MinWidth="400"/>
</Grid.ColumnDefinitions>

<!-- Master list -->
<DockPanel Grid.Column="0">
<TextBox DockPanel.Dock="Top"
Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
local:ValidationAttached.Watermark="Search payments..."/>

<ListBox ItemsSource="{Binding FilteredPayments}"
SelectedItem="{Binding SelectedPayment}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Style="{StaticResource PaymentItemStyle}">
<ContentPresenter Content="{Binding}"
ContentTemplate="{StaticResource PaymentHeaderTemplate}"/>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>

<!-- Splitter -->
<GridSplitter Grid.Column="1"
HorizontalAlignment="Stretch"
Background="LightGray"/>

<!-- Detail view -->
<ScrollViewer Grid.Column="2" VerticalScrollBarVisibility="Auto">
<ContentControl Content="{Binding SelectedPayment}">
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedPayment}" Value="{x:Null}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="Select a payment to view details"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="Gray"/>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedPayment}" Value="{x:NotNull}">
<Setter Property="ContentTemplate"
Value="{StaticResource PaymentDetailTemplate}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</ScrollViewer>
</Grid>

Best Practices

  1. Create reusable components for common UI patterns
  2. Use dependency properties for data binding in custom controls
  3. Implement value converters for data transformation
  4. Organize resources in separate dictionaries
  5. Use behaviors for reusable interaction logic
  6. Apply consistent styling through templates
  7. Test components in isolation

Common Pitfalls

  1. Not using weak events in attached properties
  2. Creating converters that throw exceptions
  3. Forgetting to implement ConvertBack when needed
  4. Memory leaks from event subscriptions
  5. Overly complex control templates

Summary

The UI components in the AR Payment Reversal dashboard provide a rich set of reusable building blocks that ensure consistency, maintainability, and professional appearance throughout the application. Through custom controls, converters, behaviors, and templates, the application delivers a sophisticated user experience while maintaining clean separation of concerns and testability.