Skip to main content

GST WPF MVVM Development Pattern Guide

This document describes the WPF MVVM architecture, ViewModel patterns, DI integration, and UI development standards of the GST Framework. Applicable to ProcessVision, Jope-SMB, PLC-Monitor, and InspectionHost.

1. Architecture Overview

┌──────────────────────────────────────────────────┐
│ View (XAML) │
│ WindowBase / DialogWindowBase / MainWindowBase │
├──────────────────────────────────────────────────┤
│ ViewModel │
│ WpfViewModelBase / ObservableObject │
│ + CommunityToolkit.Mvvm Source Generators │
├──────────────────────────────────────────────────┤
│ Services (DI Registered) │
│ IDialogService / INavigationService / IMessageBus│
├──────────────────────────────────────────────────┤
│ GST.UI.Wpf / GST.UI.Abstractions │
│ Base components, value converters, commands, │
│ validation │
└──────────────────────────────────────────────────┘

2. ViewModel Base Classes

2.1 ViewModelBase (GST.UI.Abstractions)

public abstract class ViewModelBase : INotifyPropertyChanged, IDisposable
{
// Property change notification
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? name = null);
protected bool SetProperty<T>(ref T field, T value, Action onChanged, [CallerMemberName] string? name = null);

// Lifecycle
protected virtual void OnInitialize();
protected virtual Task OnInitializeAsync();
protected virtual void OnDispose();

// Busy state (for long-running operations)
protected void SetBusy(string? message = null);
protected void ClearBusy();
protected Task ExecuteWithBusyAsync<T>(Func<Task<T>> operation, string? message = null);
}

2.2 WpfViewModelBase (GST.UI.Wpf)

Inherits from ViewModelBase, adds UI thread safety:

public abstract class WpfViewModelBase : ViewModelBase
{
protected Dispatcher Dispatcher { get; }

// UI thread safe invocation
protected void InvokeOnUIThread(Action action);
protected T InvokeOnUIThread<T>(Func<T> func);
protected Task InvokeOnUIThreadAsync(Action action);

// OnPropertyChanged automatically marshals to UI thread
protected override void OnPropertyChanged(string? propertyName);
}
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
private string _windowTitle = "Process Vision";

[ObservableProperty]
private bool _isRunning;

[RelayCommand]
private void Exit() => Application.Current.Shutdown();

[RelayCommand]
private async Task StartProcessAsync() { /* ... */ }

// Auto-generated hook
partial void OnIsRunningChanged(bool value)
{
// Called automatically when IsRunning changes
}
}
AttributeGeneratesPurpose
[ObservableProperty]Property + PropertyChangedReplaces manual SetProperty
[RelayCommand]ICommand PropertyReplaces manual new RelayCommand
partial void On{Name}ChangedProperty change hook

3. Command Patterns

3.1 RelayCommand (Synchronous)

public ICommand SaveCommand => new RelayCommand(
execute: () => Save(),
canExecute: () => IsValid
);

3.2 AsyncRelayCommand (Asynchronous)

public ICommand LoadDataCommand => new AsyncRelayCommand(
execute: async () => await LoadDataAsync(),
canExecute: () => !IsBusy
);
// IsExecuting flag prevents duplicate execution

4. DI Registration

4.1 Extension Methods

services.AddWpfUI()                              // Core WPF services
.AddView<IMainView, MainWindow>() // View (Transient)
.AddSingletonView<ISplashView, SplashWindow>() // View (Singleton)
.AddViewModel<MainViewModel>() // ViewModel (Transient)
.AddSingletonViewModel<StatusViewModel>() // ViewModel (Singleton)
.AddViewWithViewModel<SettingsView, SettingsViewModel>(); // Paired registration

4.2 App.xaml.cs Startup Pattern

protected override async void OnStartup(StartupEventArgs e)
{
// 1. Pre-DI (License, Serilog bootstrap)
// 2. Background initialization (Host.CreateDefaultBuilder, DB Migration)
// 3. DI registration (Services, ViewModels, Views)
// 4. LocalizationServiceLocator.Initialize(Services)
// 5. UI creation (MainWindow + MainViewModel + DataContext)
// 6. Deferred tasks (update check, background services)
}

5. View Base Classes

5.1 WindowBase

public class WindowBase : Window, IView
{
// Dispatcher safe invocation
protected void InvokeIfRequired(Action action);
protected T InvokeIfRequired<T>(Func<T> func);
protected Task InvokeAsync(Action action);

// Message dialogs
public void ShowInfo(string title, string message);
public void ShowWarning(string title, string message);
public void ShowError(string title, string message);
public bool ShowConfirmation(string title, string message);
}

5.2 Derived Classes

ClassPurpose
MainWindowBaseMain window
DialogWindowBaseDialog window

6. Service Interfaces

6.1 IDialogService

void ShowInfo/ShowWarning/ShowError(string title, string message);
bool ShowConfirmation(string title, string message);
string? ShowInput(string title, string prompt, string? defaultValue);
DialogResult ShowDialog<TView>() where TView : IDialogView;
string? ShowOpenFileDialog(FileDialogOptions? options);
string? ShowSaveFileDialog(FileDialogOptions? options);
string? ShowFolderBrowserDialog(FolderDialogOptions? options);

6.2 INavigationService

TView NavigateTo<TView>() where TView : IView;
TView NavigateTo<TView>(NavigationParameters parameters);
bool GoBack();
bool GoForward();
void ClearHistory();
event EventHandler<NavigatingEventArgs>? Navigating; // Cancellable
event EventHandler<NavigatedEventArgs>? Navigated;

6.3 IMessageBus

void Publish<TMessage>(TMessage message);
IDisposable Subscribe<TMessage>(Action<TMessage> handler);
IDisposable Subscribe<TMessage>(Action<TMessage> handler, Predicate<TMessage> filter);

Cross-ViewModel communication, decoupled.

7. Validation Pattern

public abstract class ValidatableViewModelBase : ViewModelBase, IValidatable
{
public bool HasErrors { get; }
public bool IsValid => !HasErrors;

protected void AddError(string propertyName, string error);
protected bool SetPropertyAndValidate<T>(ref T field, T value);
protected virtual void OnValidate();
protected virtual void OnValidateProperty(string propertyName);
}

The WPF version WpfValidatableViewModelBase automatically marshals error notifications to the UI thread.

8. Value Converters

8.1 Built-in Converters

ConverterPurpose
BooleanToVisibilityConverterbool → Visible/Collapsed
InverseBooleanToVisibilityConverterbool → Collapsed/Visible
NullToVisibilityConverternull → Collapsed

8.2 Custom Converter Example

[ValueConversion(typeof(bool), typeof(Visibility))]
public class BooleanToVisibilityConverter : IValueConverter
{
public Visibility TrueValue { get; set; } = Visibility.Visible;
public Visibility FalseValue { get; set; } = Visibility.Collapsed;

public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
=> value is true ? TrueValue : FalseValue;
}

9. UI Thread Safety Summary

ScenarioLocationPattern
Background thread updating PropertyWpfViewModelBaseOnPropertyChanged auto-dispatches
UI thread checkWindowBaseDispatcher.CheckAccess()
Long-running async operationsExecuteWithBusyAsync()Task-based, sets IsBusy
Event subscriptionViewModel constructorUnsubscribe in Cleanup()
DispatcherTimerUI thread timerStart in constructor

10. Development Standards

10.1 ViewModel Design

  • All dependencies through constructor injection
  • Use [ObservableProperty] instead of manual SetProperty()
  • Use [RelayCommand] instead of manual new RelayCommand()
  • Provide a Cleanup() method to unsubscribe from events

10.2 View Design

  • Views contain no business logic
  • DataContext is set via DI or ViewFactory
  • All UI text must use {l:L key} Localization

10.3 Asynchronous Operations

  • Use AsyncRelayCommand (prevents duplicate execution)
  • Use ExecuteWithBusyAsync() for long-running operations (shows busy state)
  • Avoid async void (only for event handlers)

Document Version: v1.0 | Created: 2026-03-16 | Related Issue: GST-166