Data Presentation
Overview
This document details the data presentation patterns in the AR Payment Reversal dashboard, including data grid implementations, paging patterns with PagedDataViewModel, data filtering and sorting mechanisms, data validation UI, loading states and progress indicators, error message presentation, and data formatting with localization support.
Key Concepts
- Data Virtualization: Efficient handling of large datasets
- Paging Strategies: Breaking data into manageable chunks
- Dynamic Filtering: Real-time data filtering
- Sort Indicators: Visual feedback for sorting
- Validation Feedback: Inline and summary validation display
- Loading States: User feedback during data operations
- Localization: Culture-specific formatting
Implementation Details
Data Grid Implementation
Telerik RadGridView Configuration
<!-- MepApps.Dash.Ar.Maint.PaymentReversal/Views/ArReversePaymentQueueView.xaml -->
<telerik:RadGridView
x:Name="PaymentQueueGrid"
ItemsSource="{Binding ReversePaymentQueueHeaders}"
SelectedItem="{Binding SelectedReversePaymentQueueHeader}"
AutoGenerateColumns="False"
CanUserSortColumns="True"
CanUserReorderColumns="True"
CanUserResizeColumns="True"
ShowGroupPanel="True"
ShowColumnFooters="True"
EnableRowVirtualization="True"
EnableColumnVirtualization="True"
RowIndicatorVisibility="Visible"
GridLinesVisibility="Both"
AlternateRowBackground="#F5F5F5"
SelectionMode="Extended">
<!-- Column definitions -->
<telerik:RadGridView.Columns>
<!-- Checkbox column for multi-select -->
<telerik:GridViewCheckBoxColumn
DataMemberBinding="{Binding Selected}"
Header=""
Width="30"
IsReadOnly="False">
<telerik:GridViewCheckBoxColumn.HeaderCellStyle>
<Style TargetType="telerik:GridViewHeaderCell">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<CheckBox IsChecked="{Binding DataContext.SelectAll,
RelativeSource={RelativeSource AncestorType=UserControl}}"
HorizontalAlignment="Center"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</telerik:GridViewCheckBoxColumn.HeaderCellStyle>
</telerik:GridViewCheckBoxColumn>
<!-- Data columns with formatting -->
<telerik:GridViewDataColumn
DataMemberBinding="{Binding Customer}"
Header="Customer"
Width="100"
ShowDistinctFilters="True">
<telerik:GridViewDataColumn.AggregateFunctions>
<telerik:CountFunction Caption="Count: "/>
</telerik:GridViewDataColumn.AggregateFunctions>
</telerik:GridViewDataColumn>
<telerik:GridViewDataColumn
DataMemberBinding="{Binding CheckNumber}"
Header="Check #"
Width="100"
IsFilterable="True">
<telerik:GridViewDataColumn.FilterMemberPath>CheckNumber</telerik:GridViewDataColumn.FilterMemberPath>
</telerik:GridViewDataColumn>
<telerik:GridViewDataColumn
DataMemberBinding="{Binding CheckValue}"
Header="Amount"
Width="120"
DataFormatString="{}{0:C}"
TextAlignment="Right">
<telerik:GridViewDataColumn.AggregateFunctions>
<telerik:SumFunction Caption="Total: " SourceField="CheckValue"
ResultFormatString="{}{0:C}"/>
</telerik:GridViewDataColumn.AggregateFunctions>
</telerik:GridViewDataColumn>
<telerik:GridViewDataColumn
DataMemberBinding="{Binding PaymentDate}"
Header="Date"
Width="100"
DataFormatString="{}{0:d}"
SortMemberPath="PaymentDate"/>
<!-- Template column for complex display -->
<telerik:GridViewDataColumn Header="Status" Width="150">
<telerik:GridViewDataColumn.CellTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Ellipse Width="8" Height="8" Margin="0,0,5,0">
<Ellipse.Fill>
<Binding Path="Status"
Converter="{StaticResource StatusToColorConverter}"/>
</Ellipse.Fill>
</Ellipse>
<TextBlock Grid.Column="1" Text="{Binding Status}"/>
</Grid>
</DataTemplate>
</telerik:GridViewDataColumn.CellTemplate>
</telerik:GridViewDataColumn>
</telerik:RadGridView.Columns>
<!-- Row details template -->
<telerik:RadGridView.RowDetailsTemplate>
<DataTemplate>
<Border Background="LightYellow" Padding="10">
<StackPanel>
<TextBlock Text="Additional Details:" FontWeight="Bold"/>
<TextBlock Text="{Binding Bank, StringFormat='Bank: {0}'}"/>
<TextBlock Text="{Binding TrnYear, StringFormat='Year: {0}'}"/>
<TextBlock Text="{Binding TrnMonth, StringFormat='Month: {0}'}"/>
<TextBlock Text="{Binding Journal, StringFormat='Journal: {0}'}"/>
</StackPanel>
</Border>
</DataTemplate>
</telerik:RadGridView.RowDetailsTemplate>
<!-- Group header template -->
<telerik:RadGridView.GroupHeaderTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Group.Key}" FontWeight="Bold"/>
<TextBlock Text=" (" Margin="5,0,0,0"/>
<TextBlock Text="{Binding Group.ItemCount}"/>
<TextBlock Text=" items)"/>
</StackPanel>
</DataTemplate>
</telerik:RadGridView.GroupHeaderTemplate>
</telerik:RadGridView>
Paging Implementation
PagedDataViewModel
// MepApps.Dash.Ar.Maint.PaymentReversal/Models/BindingModels/PagedDataViewModel.cs
public class PagedDataViewModel<T> : BaseViewModel
{
private readonly Func<int, int, Task<PagedResult<T>>> _loadPageFunc;
private ObservableCollection<T> _items;
private int _currentPage = 1;
private int _pageSize = 50;
private int _totalItems;
private bool _isLoading;
public PagedDataViewModel(Func<int, int, Task<PagedResult<T>>> loadPageFunc)
{
_loadPageFunc = loadPageFunc;
FirstPageCommand = new RelayCommandAsync(GoToFirstPage, () => CanGoToPreviousPage);
PreviousPageCommand = new RelayCommandAsync(GoToPreviousPage, () => CanGoToPreviousPage);
NextPageCommand = new RelayCommandAsync(GoToNextPage, () => CanGoToNextPage);
LastPageCommand = new RelayCommandAsync(GoToLastPage, () => CanGoToNextPage);
RefreshCommand = new RelayCommandAsync(RefreshCurrentPage);
PageSizes = new[] { 25, 50, 100, 200 };
}
public ObservableCollection<T> Items
{
get => _items ??= new ObservableCollection<T>();
private set => SetField(ref _items, value);
}
public int CurrentPage
{
get => _currentPage;
private set
{
if (SetField(ref _currentPage, value))
{
OnPropertyChanged(nameof(PageInfo));
UpdateCommands();
}
}
}
public int PageSize
{
get => _pageSize;
set
{
if (SetField(ref _pageSize, value))
{
_ = LoadPage(1); // Reset to first page when page size changes
}
}
}
public int TotalItems
{
get => _totalItems;
private set
{
if (SetField(ref _totalItems, value))
{
OnPropertyChanged(nameof(TotalPages));
OnPropertyChanged(nameof(PageInfo));
}
}
}
public int TotalPages => (int)Math.Ceiling(TotalItems / (double)PageSize);
public string PageInfo => $"Page {CurrentPage} of {TotalPages} ({TotalItems} total items)";
public bool IsLoading
{
get => _isLoading;
private set => SetField(ref _isLoading, value);
}
public int[] PageSizes { get; }
// Navigation commands
public ICommand FirstPageCommand { get; }
public ICommand PreviousPageCommand { get; }
public ICommand NextPageCommand { get; }
public ICommand LastPageCommand { get; }
public ICommand RefreshCommand { get; }
private bool CanGoToPreviousPage => CurrentPage > 1 && !IsLoading;
private bool CanGoToNextPage => CurrentPage < TotalPages && !IsLoading;
public async Task LoadPage(int pageNumber)
{
if (IsLoading)
return;
try
{
IsLoading = true;
var result = await _loadPageFunc(pageNumber, PageSize);
if (result != null)
{
Items.Clear();
foreach (var item in result.Items)
{
Items.Add(item);
}
CurrentPage = pageNumber;
TotalItems = result.TotalCount;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading page {PageNumber}", pageNumber);
throw;
}
finally
{
IsLoading = false;
}
}
private async Task GoToFirstPage() => await LoadPage(1);
private async Task GoToPreviousPage() => await LoadPage(CurrentPage - 1);
private async Task GoToNextPage() => await LoadPage(CurrentPage + 1);
private async Task GoToLastPage() => await LoadPage(TotalPages);
private async Task RefreshCurrentPage() => await LoadPage(CurrentPage);
private void UpdateCommands()
{
(FirstPageCommand as RelayCommandAsync)?.RaiseCanExecuteChanged();
(PreviousPageCommand as RelayCommandAsync)?.RaiseCanExecuteChanged();
(NextPageCommand as RelayCommandAsync)?.RaiseCanExecuteChanged();
(LastPageCommand as RelayCommandAsync)?.RaiseCanExecuteChanged();
}
}
public class PagedResult<T>
{
public IEnumerable<T> Items { get; set; }
public int TotalCount { get; set; }
public int PageNumber { get; set; }
public int PageSize { get; set; }
}
Data Filtering and Sorting
Filter View Model
public class FilterableDataViewModel<T> : BaseViewModel
{
private readonly ObservableCollection<T> _allItems;
private ObservableCollection<T> _filteredItems;
private string _filterText;
private string _sortProperty;
private ListSortDirection _sortDirection;
public FilterableDataViewModel(IEnumerable<T> items)
{
_allItems = new ObservableCollection<T>(items);
_filteredItems = new ObservableCollection<T>(_allItems);
FilterProperties = typeof(T).GetProperties()
.Where(p => p.PropertyType == typeof(string) ||
p.PropertyType == typeof(int) ||
p.PropertyType == typeof(decimal))
.Select(p => p.Name)
.ToList();
}
public ObservableCollection<T> FilteredItems
{
get => _filteredItems;
private set => SetField(ref _filteredItems, value);
}
public string FilterText
{
get => _filterText;
set
{
if (SetField(ref _filterText, value))
{
ApplyFilter();
}
}
}
public List<string> FilterProperties { get; }
public string SelectedFilterProperty { get; set; }
private void ApplyFilter()
{
if (string.IsNullOrWhiteSpace(FilterText))
{
FilteredItems = new ObservableCollection<T>(_allItems);
return;
}
var filtered = _allItems.Where(item =>
{
if (string.IsNullOrEmpty(SelectedFilterProperty))
{
// Search all string properties
return typeof(T).GetProperties()
.Where(p => p.PropertyType == typeof(string))
.Any(p => p.GetValue(item)?.ToString()?.Contains(FilterText,
StringComparison.OrdinalIgnoreCase) ?? false);
}
else
{
// Search specific property
var prop = typeof(T).GetProperty(SelectedFilterProperty);
var value = prop?.GetValue(item)?.ToString();
return value?.Contains(FilterText, StringComparison.OrdinalIgnoreCase) ?? false;
}
});
FilteredItems = new ObservableCollection<T>(filtered);
ApplySorting();
}
public void Sort(string propertyName)
{
if (_sortProperty == propertyName)
{
_sortDirection = _sortDirection == ListSortDirection.Ascending
? ListSortDirection.Descending
: ListSortDirection.Ascending;
}
else
{
_sortProperty = propertyName;
_sortDirection = ListSortDirection.Ascending;
}
ApplySorting();
}
private void ApplySorting()
{
if (string.IsNullOrEmpty(_sortProperty))
return;
var prop = typeof(T).GetProperty(_sortProperty);
if (prop == null)
return;
var sorted = _sortDirection == ListSortDirection.Ascending
? FilteredItems.OrderBy(x => prop.GetValue(x))
: FilteredItems.OrderByDescending(x => prop.GetValue(x));
FilteredItems = new ObservableCollection<T>(sorted);
}
}
Data Validation UI
Validation Templates
<!-- Validation error template -->
<ControlTemplate x:Key="ValidationErrorTemplate">
<DockPanel>
<Border BorderBrush="Red" BorderThickness="1">
<AdornedElementPlaceholder/>
</Border>
<TextBlock DockPanel.Dock="Right"
Foreground="Red"
FontSize="16"
Text="!"
FontWeight="Bold"
Margin="5,0,0,0"
ToolTip="{Binding ElementName=adornedElement,
Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"/>
</DockPanel>
</ControlTemplate>
<!-- Validation summary -->
<ItemsControl ItemsSource="{Binding ValidationErrors}"
Visibility="{Binding HasValidationErrors,
Converter={StaticResource BoolToVisibilityConverter}}">
<ItemsControl.Template>
<ControlTemplate>
<Border Background="#FFFFE0"
BorderBrush="Red"
BorderThickness="1"
CornerRadius="3"
Margin="5"
Padding="10">
<StackPanel>
<TextBlock Text="Please correct the following errors:"
FontWeight="Bold"
Foreground="Red"
Margin="0,0,0,5"/>
<ItemsPresenter/>
</StackPanel>
</Border>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" Foreground="Red" Margin="0,2"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Loading States and Progress Indicators
Loading Overlay Component
<!-- Loading overlay -->
<Grid x:Name="LoadingOverlay"
Visibility="{Binding IsLoading, Converter={StaticResource BoolToVisibilityConverter}}"
Background="#80000000"
Panel.ZIndex="999">
<Border Background="White"
CornerRadius="10"
Width="300"
Height="150"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Progress ring -->
<telerik:RadProgressBar Grid.Row="0"
IsIndeterminate="{Binding IsIndeterminate}"
Value="{Binding ProgressValue}"
Maximum="{Binding ProgressMaximum}"
Height="20"/>
<!-- Status text -->
<TextBlock Grid.Row="1"
Text="{Binding LoadingMessage}"
HorizontalAlignment="Center"
Margin="0,10,0,5"
FontSize="14"/>
<!-- Details -->
<TextBlock Grid.Row="2"
Text="{Binding LoadingDetails}"
HorizontalAlignment="Center"
Foreground="Gray"
FontSize="11"/>
</Grid>
</Border>
</Grid>
Progress View Model
public class ProgressViewModel : BaseViewModel
{
private bool _isLoading;
private bool _isIndeterminate = true;
private double _progressValue;
private double _progressMaximum = 100;
private string _loadingMessage = "Loading...";
private string _loadingDetails;
public bool IsLoading
{
get => _isLoading;
set => SetField(ref _isLoading, value);
}
public bool IsIndeterminate
{
get => _isIndeterminate;
set => SetField(ref _isIndeterminate, value);
}
public double ProgressValue
{
get => _progressValue;
set
{
if (SetField(ref _progressValue, value))
{
OnPropertyChanged(nameof(ProgressPercentage));
UpdateLoadingDetails();
}
}
}
public double ProgressMaximum
{
get => _progressMaximum;
set => SetField(ref _progressMaximum, value);
}
public double ProgressPercentage => (ProgressValue / ProgressMaximum) * 100;
public string LoadingMessage
{
get => _loadingMessage;
set => SetField(ref _loadingMessage, value);
}
public string LoadingDetails
{
get => _loadingDetails;
set => SetField(ref _loadingDetails, value);
}
private void UpdateLoadingDetails()
{
if (!IsIndeterminate)
{
LoadingDetails = $"{ProgressValue:0} of {ProgressMaximum:0} ({ProgressPercentage:0}%)";
}
}
public async Task ExecuteWithProgress<T>(
IEnumerable<T> items,
Func<T, Task> operation,
string message = "Processing...")
{
IsLoading = true;
IsIndeterminate = false;
LoadingMessage = message;
var itemList = items.ToList();
ProgressMaximum = itemList.Count;
ProgressValue = 0;
try
{
foreach (var item in itemList)
{
await operation(item);
ProgressValue++;
}
}
finally
{
IsLoading = false;
}
}
}
Error Message Presentation
Error Display Strategies
<!-- Inline error display -->
<StackPanel>
<TextBox Text="{Binding CustomerCode, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}">
<TextBox.Style>
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="Background" Value="#FFFFE0"/>
<Setter Property="BorderBrush" Value="Red"/>
<Setter Property="BorderThickness" Value="2"/>
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<TextBlock Text="{Binding (Validation.Errors)[0].ErrorContent,
ElementName=CustomerCodeTextBox}"
Foreground="Red"
FontSize="11"
Margin="0,2,0,0"
Visibility="{Binding (Validation.HasError),
ElementName=CustomerCodeTextBox,
Converter={StaticResource BoolToVisibilityConverter}}"/>
</StackPanel>
<!-- Toast notification for errors -->
<telerik:RadNotificationManager x:Name="NotificationManager"/>
// Error notification service
public class ErrorNotificationService
{
private readonly RadNotificationManager _notificationManager;
public void ShowError(string message, Exception exception = null)
{
var notification = new RadNotification
{
Header = "Error",
Content = message,
Icon = new BitmapImage(new Uri("/Images/error.png", UriKind.Relative)),
ShowDuration = 5000,
Background = Brushes.LightPink,
BorderBrush = Brushes.Red
};
if (exception != null)
{
notification.ToolTip = exception.ToString();
}
_notificationManager.ShowNotification(notification);
}
public void ShowWarning(string message)
{
var notification = new RadNotification
{
Header = "Warning",
Content = message,
Icon = new BitmapImage(new Uri("/Images/warning.png", UriKind.Relative)),
ShowDuration = 3000,
Background = Brushes.LightYellow,
BorderBrush = Brushes.Orange
};
_notificationManager.ShowNotification(notification);
}
public void ShowSuccess(string message)
{
var notification = new RadNotification
{
Header = "Success",
Content = message,
Icon = new BitmapImage(new Uri("/Images/success.png", UriKind.Relative)),
ShowDuration = 2000,
Background = Brushes.LightGreen,
BorderBrush = Brushes.Green
};
_notificationManager.ShowNotification(notification);
}
}
Data Formatting and Localization
Format Providers
public class CultureAwareFormatProvider : IFormatProvider, ICustomFormatter
{
private readonly CultureInfo _culture;
public CultureAwareFormatProvider(CultureInfo culture = null)
{
_culture = culture ?? CultureInfo.CurrentCulture;
}
public object GetFormat(Type formatType)
{
return formatType == typeof(ICustomFormatter) ? this : null;
}
public string Format(string format, object arg, IFormatProvider formatProvider)
{
if (arg == null)
return string.Empty;
// Handle currency formatting
if (format?.StartsWith("C") == true && arg is decimal decimalValue)
{
return decimalValue.ToString("C", _culture);
}
// Handle date formatting
if (arg is DateTime dateTime)
{
return format switch
{
"short" => dateTime.ToString("d", _culture),
"long" => dateTime.ToString("D", _culture),
"time" => dateTime.ToString("t", _culture),
_ => dateTime.ToString(format ?? "G", _culture)
};
}
// Handle number formatting
if (arg is IFormattable formattable)
{
return formattable.ToString(format, _culture);
}
return arg.ToString();
}
}
// Binding converter for localized formatting
public class LocalizedFormatConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
return string.Empty;
var format = parameter as string ?? "G";
var formatProvider = new CultureAwareFormatProvider(culture);
return string.Format(formatProvider, $"{{0:{format}}}", value);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Best Practices
- Use virtualization for large datasets
- Implement paging for better performance
- Provide clear loading indicators for async operations
- Use consistent formatting across the application
- Show validation errors immediately and clearly
- Support keyboard navigation in grids
- Test with different cultures for localization
Common Pitfalls
- Loading all data at once without paging
- Missing loading indicators for long operations
- Unclear validation messages that don't help users
- Hardcoded formatting instead of culture-aware
- Not handling empty states in data displays
Related Documentation
- UI Architecture - Overall UI design
- UI Components - Reusable controls
- MVVM Patterns - Data binding patterns
- User Interactions - Input handling
Summary
The data presentation layer in the AR Payment Reversal dashboard provides comprehensive patterns for displaying, formatting, and interacting with data. Through proper use of virtualization, paging, filtering, and formatting, the application delivers a responsive and user-friendly experience even with large datasets, while maintaining clear validation feedback and loading states.