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);
}
2.3 CommunityToolkit.Mvvm (Recommended Usage)
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
}
}
| Attribute | Generates | Purpose |
|---|---|---|
[ObservableProperty] | Property + PropertyChanged | Replaces manual SetProperty |
[RelayCommand] | ICommand Property | Replaces manual new RelayCommand |
partial void On{Name}Changed | — | Property 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
| Class | Purpose |
|---|---|
MainWindowBase | Main window |
DialogWindowBase | Dialog 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
| Converter | Purpose |
|---|---|
BooleanToVisibilityConverter | bool → Visible/Collapsed |
InverseBooleanToVisibilityConverter | bool → Collapsed/Visible |
NullToVisibilityConverter | null → 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
| Scenario | Location | Pattern |
|---|---|---|
| Background thread updating Property | WpfViewModelBase | OnPropertyChanged auto-dispatches |
| UI thread check | WindowBase | Dispatcher.CheckAccess() |
| Long-running async operations | ExecuteWithBusyAsync() | Task-based, sets IsBusy |
| Event subscription | ViewModel constructor | Unsubscribe in Cleanup() |
| DispatcherTimer | UI thread timer | Start in constructor |
10. Development Standards
10.1 ViewModel Design
- All dependencies through constructor injection
- Use
[ObservableProperty]instead of manualSetProperty() - Use
[RelayCommand]instead of manualnew 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