Skip to main content

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

  1. Follow MVVM pattern strictly - no code-behind logic
  2. Use data binding for all UI updates
  3. Centralize styles in resource dictionaries
  4. Implement responsive layouts for different screen sizes
  5. Use virtualization for large data sets
  6. Leverage Telerik controls for advanced functionality
  7. Test UI on different DPI settings

Common Pitfalls

  1. Mixing concerns - business logic in views
  2. Not using virtualization for grids
  3. Hardcoding styles instead of using resources
  4. Ignoring memory leaks from event handlers
  5. Poor performance from excessive binding updates

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.