UI Architecture
Overview
This document explains the overall UI architecture of the AR Payment Reversal dashboard, including the WPF/MVVM implementation, View-ViewModel binding patterns, component hierarchy, view structure, styling and theming approaches, responsive design patterns, and UI composition strategies. The architecture ensures a maintainable, testable, and user-friendly interface that integrates seamlessly with SYSPRO's UI guidelines.
Key Concepts
- WPF Framework: Windows Presentation Foundation for rich desktop UI
- MVVM Pattern: Model-View-ViewModel architectural pattern
- Data Binding: Declarative UI-to-data connections
- Component Hierarchy: Structured view organization
- Resource Dictionaries: Centralized styling and templates
- Telerik Controls: Enhanced UI components
- UI Composition: Building complex UIs from simple components
Implementation Details
WPF/MVVM Architecture
Application Structure
// MepApps.Dash.Ar.Maint.PaymentReversal/Views/MainView.xaml.cs (Lines 13-50)
namespace MepApps.Dash.Ar.Maint.PaymentReversal.Views
{
public partial class MainView : UserControl
{
// MVVM ViewModel binding
private MainViewModel viewmodel = null;
// WPF Dispatcher for UI thread operations
private readonly Dispatcher _dispatcher;
// SYSPRO integration interfaces
private readonly IContainerEvents _events;
private readonly IContainerNavigation _navigation;
private readonly IMepPluginServiceHandler _mepPluginServiceHandler;
private readonly ISharedShellInterface _sharedShellInterface;
public MainView(
ISharedShellInterface sharedShellInterface,
IContainerEvents events,
IContainerNavigation navigation,
IMepPluginServiceHandler mepPluginServiceHandler)
{
InitializeComponent();
_dispatcher = Dispatcher.CurrentDispatcher;
// Initialize services
InitializeServices();
// Set up data context for MVVM binding
SetupDataContext();
// Wire up events
WireUpEvents();
}
private void SetupDataContext()
{
viewmodel = _mepPluginServiceProvider.GetService<MainViewModel>();
this.DataContext = viewmodel;
// Initialize view model asynchronously
Dispatcher.BeginInvoke(new Action(async () =>
{
await viewmodel.InitializeAsync();
}));
}
}
}
View-ViewModel Binding Patterns
XAML Data Binding
<!-- MepApps.Dash.Ar.Maint.PaymentReversal/Views/MainView.xaml (Lines 1-42) -->
<UserControl
x:Class="MepApps.Dash.Ar.Maint.PaymentReversal.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
d:DesignHeight="800"
d:DesignWidth="1200">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MepApps.Dash.Ar.Maint.PaymentReversal;component/Resources/ResourceDictionary.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Loading indicator with data binding -->
<telerik:RadBusyIndicator
IsBusy="{Binding IsInitializing}"
BusyContent="Initializing..."
Grid.Row="1">
<!-- Main window container -->
<telerik:RadWindow
x:Name="MainWindow"
Content="{Binding MainContent}"
Header="{Binding Title}"
WindowState="Maximized">
<!-- Content presenter for dynamic views -->
<ContentPresenter />
</telerik:RadWindow>
</telerik:RadBusyIndicator>
</Grid>
</UserControl>
Component Hierarchy
View Organization Structure
MainView (Root Container)
├── RadWindow (Window Chrome)
│ └── ContentPresenter (Dynamic Content Host)
│ ├── ArReversePaymentQueueView (Default View)
│ │ ├── ToolBar (Actions)
│ │ ├── RadGridView (Payment List)
│ │ └── DetailPanel (Invoice Details)
│ ├── ArReversePaymentAddPaymentView
│ │ ├── CustomerSelector
│ │ ├── PaymentGrid
│ │ └── InvoiceDetails
│ └── ArReversePaymentCompletionView
│ ├── SummaryPanel
│ ├── ResultsGrid
│ └── ExportOptions
└── Dialogs (Modal Overlays)
├── CustomerSelectionDialog
└── SettingsDialog
View Structure and Composition
Queue View Implementation
<!-- MepApps.Dash.Ar.Maint.PaymentReversal/Views/ArReversePaymentQueueView.xaml -->
<UserControl x:Class="MepApps.Dash.Ar.Maint.PaymentReversal.Views.ArReversePaymentQueueView">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <!-- Toolbar -->
<RowDefinition Height="*"/> <!-- Main Content -->
<RowDefinition Height="Auto"/> <!-- Status Bar -->
</Grid.RowDefinitions>
<!-- Toolbar Section -->
<ToolBarTray Grid.Row="0">
<ToolBar>
<Button Command="{Binding AddPaymentCmd}"
Style="{StaticResource ToolbarButtonStyle}">
<StackPanel Orientation="Horizontal">
<Image Source="{StaticResource AddIcon}" Width="16" Height="16"/>
<TextBlock Text="Add Payment" Margin="5,0,0,0"/>
</StackPanel>
</Button>
<Separator/>
<Button Command="{Binding GetQueuedPaymentsCmd}"
IsEnabled="{Binding GetQueuedPaymentsEnabled}">
<StackPanel Orientation="Horizontal">
<Image Source="{StaticResource RefreshIcon}" Width="16" Height="16"/>
<TextBlock Text="Refresh Queue"/>
</StackPanel>
</Button>
<Button Command="{Binding ClearPaymentsCmd}"
IsEnabled="{Binding ClearPaymentsEnabled}">
<StackPanel Orientation="Horizontal">
<Image Source="{StaticResource ClearIcon}" Width="16" Height="16"/>
<TextBlock Text="Clear Queue"/>
</StackPanel>
</Button>
<Separator/>
<Button Command="{Binding ReversePaymentsCmd}"
IsEnabled="{Binding ReversePaymentsEnabled}"
Style="{StaticResource PrimaryActionButtonStyle}">
<StackPanel Orientation="Horizontal">
<Image Source="{StaticResource PostIcon}" Width="16" Height="16"/>
<TextBlock Text="Post Reversals" FontWeight="Bold"/>
</StackPanel>
</Button>
</ToolBar>
</ToolBarTray>
<!-- Main Content Area -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<!-- Payment Queue Grid -->
<GroupBox Grid.Column="0" Header="Queued Payments" Margin="5">
<telerik:RadGridView
x:Name="PaymentQueueGrid"
ItemsSource="{Binding ReversePaymentQueueHeaders}"
SelectedItem="{Binding SelectedReversePaymentQueueHeader}"
AutoGenerateColumns="False"
CanUserSortColumns="True"
CanUserReorderColumns="True"
ShowGroupPanel="True"
EnableRowVirtualization="True">
<telerik:RadGridView.Columns>
<telerik:GridViewCheckBoxColumn
DataMemberBinding="{Binding Selected}"
Header="Select"
Width="60"/>
<telerik:GridViewDataColumn
DataMemberBinding="{Binding Customer}"
Header="Customer"
Width="100">
<telerik:GridViewDataColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Customer}"
ToolTip="{Binding CustomerName}"/>
</DataTemplate>
</telerik:GridViewDataColumn.CellTemplate>
</telerik:GridViewDataColumn>
<telerik:GridViewDataColumn
DataMemberBinding="{Binding CheckNumber}"
Header="Check #"
Width="100"/>
<telerik:GridViewDataColumn
DataMemberBinding="{Binding CheckValue, StringFormat=C}"
Header="Amount"
Width="100"
TextAlignment="Right"/>
<telerik:GridViewDataColumn
DataMemberBinding="{Binding PaymentDate, StringFormat=d}"
Header="Date"
Width="100"/>
<telerik:GridViewDataColumn
DataMemberBinding="{Binding BankDescription}"
Header="Bank"
Width="150"/>
</telerik:RadGridView.Columns>
<telerik:RadGridView.RowStyle>
<Style TargetType="telerik:GridViewRow">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectEnabled}" Value="False">
<Setter Property="Background" Value="LightGray"/>
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
</telerik:RadGridView.RowStyle>
</telerik:RadGridView>
</GroupBox>
<!-- Splitter -->
<GridSplitter Grid.Column="1" Width="5"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"/>
<!-- Invoice Details Panel -->
<GroupBox Grid.Column="2" Header="Invoice Details" Margin="5">
<telerik:RadGridView
ItemsSource="{Binding SelectedReversePaymentQueueHeader_Details}"
AutoGenerateColumns="False"
IsReadOnly="True">
<telerik:RadGridView.Columns>
<telerik:GridViewDataColumn
DataMemberBinding="{Binding Invoice}"
Header="Invoice"/>
<telerik:GridViewDataColumn
DataMemberBinding="{Binding InvoiceDate, StringFormat=d}"
Header="Date"/>
<telerik:GridViewDataColumn
DataMemberBinding="{Binding OrigAmount, StringFormat=C}"
Header="Original"/>
<telerik:GridViewDataColumn
DataMemberBinding="{Binding Balance, StringFormat=C}"
Header="Balance"/>
<telerik:GridViewDataColumn
DataMemberBinding="{Binding PaymentValue, StringFormat=C}"
Header="Payment"/>
</telerik:RadGridView.Columns>
</telerik:RadGridView>
</GroupBox>
</Grid>
<!-- Status Bar -->
<StatusBar Grid.Row="2">
<StatusBarItem>
<TextBlock Text="Total Payments: "/>
</StatusBarItem>
<StatusBarItem>
<TextBlock Text="{Binding ReversePaymentQueueHeaders.Count}"
FontWeight="Bold"/>
</StatusBarItem>
<Separator/>
<StatusBarItem>
<TextBlock Text="Total Value: "/>
</StatusBarItem>
<StatusBarItem>
<TextBlock Text="{Binding TotalReversePaymentValue, StringFormat=C}"
FontWeight="Bold"/>
</StatusBarItem>
<StatusBarItem HorizontalAlignment="Right">
<ComboBox ItemsSource="{Binding PostPeriods}"
SelectedItem="{Binding SelectedPostPeriod}"
DisplayMemberPath="Description"
Width="200"/>
</StatusBarItem>
</StatusBar>
</Grid>
</UserControl>
Styling and Theming
Resource Dictionary Structure
<!-- MepApps.Dash.Ar.Maint.PaymentReversal/Resources/ResourceDictionary.xaml -->
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
xmlns:converters="clr-namespace:MepApps.Dash.Ar.Maint.PaymentReversal.Resources.Converters">
<!-- Converters -->
<converters:NullEmptyIEnumerableToBooleanConverter x:Key="NullEmptyToBoolConverter"/>
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<!-- Colors -->
<SolidColorBrush x:Key="PrimaryColor" Color="#007ACC"/>
<SolidColorBrush x:Key="SecondaryColor" Color="#5A5A5A"/>
<SolidColorBrush x:Key="AccentColor" Color="#FF8C00"/>
<SolidColorBrush x:Key="ErrorColor" Color="#E51400"/>
<SolidColorBrush x:Key="SuccessColor" Color="#60A917"/>
<!-- Typography -->
<Style x:Key="HeaderTextStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="18"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="{StaticResource PrimaryColor}"/>
<Setter Property="Margin" Value="0,0,0,10"/>
</Style>
<Style x:Key="LabelTextStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="12"/>
<Setter Property="Foreground" Value="{StaticResource SecondaryColor}"/>
<Setter Property="Margin" Value="0,0,5,0"/>
</Style>
<!-- Button Styles -->
<Style x:Key="PrimaryButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{StaticResource PrimaryColor}"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Padding" Value="10,5"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="3">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#005A9E"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#004578"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="LightGray"/>
<Setter Property="Foreground" Value="Gray"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Grid Styles -->
<Style x:Key="GridHeaderStyle" TargetType="telerik:GridViewHeaderCell">
<Setter Property="Background" Value="#F0F0F0"/>
<Setter Property="Foreground" Value="{StaticResource SecondaryColor}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Padding" Value="5"/>
</Style>
<!-- Validation Styles -->
<Style x:Key="ValidationErrorStyle" TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="BorderBrush" Value="{StaticResource ErrorColor}"/>
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- Animation Templates -->
<Storyboard x:Key="FadeInAnimation">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="0" To="1" Duration="0:0:0.3"/>
</Storyboard>
<Storyboard x:Key="SlideInAnimation">
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)"
From="50" To="0" Duration="0:0:0.3">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</ResourceDictionary>
Responsive Design Patterns
Adaptive Layout
<UserControl>
<Grid>
<Grid.Resources>
<!-- Define responsive breakpoints -->
<Style x:Key="ResponsivePanel" TargetType="Grid">
<Style.Triggers>
<!-- Small screen -->
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self},
Path=ActualWidth,
Converter={StaticResource WidthToScreenSizeConverter}}"
Value="Small">
<Setter Property="Grid.ColumnDefinitions">
<Setter.Value>
<ColumnDefinitionCollection>
<ColumnDefinition Width="*"/>
</ColumnDefinitionCollection>
</Setter.Value>
</Setter>
</DataTrigger>
<!-- Large screen -->
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self},
Path=ActualWidth,
Converter={StaticResource WidthToScreenSizeConverter}}"
Value="Large">
<Setter Property="Grid.ColumnDefinitions">
<Setter.Value>
<ColumnDefinitionCollection>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</ColumnDefinitionCollection>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<!-- Adaptive content -->
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<Grid Style="{StaticResource ResponsivePanel}">
<!-- Content adapts based on screen size -->
</Grid>
</ScrollViewer>
</Grid>
</UserControl>
UI Composition Strategies
Component Reusability
public class ReusableComponents
{
// Customer selector component
public static UserControl CreateCustomerSelector()
{
return new UserControl
{
Content = new Grid
{
Children =
{
new TextBox { Name = "CustomerCode" },
new Button { Content = "...", Command = new SelectCustomerCommand() }
}
}
};
}
// Loading overlay component
public static UIElement CreateLoadingOverlay(string message = "Loading...")
{
return new Grid
{
Background = new SolidColorBrush(Color.FromArgb(128, 0, 0, 0)),
Children =
{
new Border
{
Background = Brushes.White,
CornerRadius = new CornerRadius(5),
Width = 200,
Height = 100,
Child = new StackPanel
{
VerticalAlignment = VerticalAlignment.Center,
Children =
{
new ProgressBar { IsIndeterminate = true },
new TextBlock { Text = message, HorizontalAlignment = HorizontalAlignment.Center }
}
}
}
}
};
}
}
Best Practices
- Follow MVVM pattern strictly - no code-behind logic
- Use data binding for all UI updates
- Centralize styles in resource dictionaries
- Implement responsive layouts for different screen sizes
- Use virtualization for large data sets
- Leverage Telerik controls for advanced functionality
- Test UI on different DPI settings
Common Pitfalls
- Mixing concerns - business logic in views
- Not using virtualization for grids
- Hardcoding styles instead of using resources
- Ignoring memory leaks from event handlers
- Poor performance from excessive binding updates
Related Documentation
- MVVM Patterns - ViewModel implementation
- UI Components - Reusable controls
- Data Presentation - Grid and data display
- User Interactions - Input handling
Summary
The UI architecture of the AR Payment Reversal dashboard leverages WPF's powerful capabilities through a clean MVVM implementation, ensuring maintainable, testable, and performant user interfaces. The combination of Telerik controls, proper data binding, and responsive design patterns creates a professional, user-friendly experience that integrates seamlessly with SYSPRO's enterprise environment.