跳至主要内容

GST WPF MVVM 開發模式指南

本文件說明 GST Framework 的 WPF MVVM 架構、ViewModel 模式、DI 整合與 UI 開發規範。 適用於 ProcessVision、Jope-SMB、PLC-Monitor、InspectionHost。

1. 架構總覽

┌──────────────────────────────────────────────────┐
│ View(XAML) │
│ WindowBase / DialogWindowBase / MainWindowBase │
├──────────────────────────────────────────────────┤
│ ViewModel │
│ WpfViewModelBase / ObservableObject │
│ + CommunityToolkit.Mvvm Source Generators │
├──────────────────────────────────────────────────┤
│ Services(DI 註冊) │
│ IDialogService / INavigationService / IMessageBus│
├──────────────────────────────────────────────────┤
│ GST.UI.Wpf / GST.UI.Abstractions │
│ 基礎元件、值轉換器、命令、驗證 │
└──────────────────────────────────────────────────┘

2. ViewModel 基底類別

2.1 ViewModelBase(GST.UI.Abstractions)

public abstract class ViewModelBase : INotifyPropertyChanged, IDisposable
{
// Property 變更通知
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);

// 生命週期
protected virtual void OnInitialize();
protected virtual Task OnInitializeAsync();
protected virtual void OnDispose();

// Busy 狀態(長時間操作用)
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)

繼承 ViewModelBase,加入 UI Thread 安全

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

// UI Thread 安全呼叫
protected void InvokeOnUIThread(Action action);
protected T InvokeOnUIThread<T>(Func<T> func);
protected Task InvokeOnUIThreadAsync(Action action);

// OnPropertyChanged 自動 marshal 到 UI Thread
protected override void OnPropertyChanged(string? propertyName);
}

2.3 CommunityToolkit.Mvvm(推薦用法)

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() { /* ... */ }

// 自動產生的 Hook
partial void OnIsRunningChanged(bool value)
{
// 當 IsRunning 變更時自動呼叫
}
}
Attribute產生用途
[ObservableProperty]Property + PropertyChanged取代手動 SetProperty
[RelayCommand]ICommand Property取代手動 new RelayCommand
partial void On{Name}ChangedProperty 變更 Hook

3. Command 模式

3.1 RelayCommand(同步)

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

3.2 AsyncRelayCommand(非同步)

public ICommand LoadDataCommand => new AsyncRelayCommand(
execute: async () => await LoadDataAsync(),
canExecute: () => !IsBusy
);
// IsExecuting 旗標防止重複執行

4. DI 註冊

4.1 Extension Methods

services.AddWpfUI()                              // 核心 WPF 服務
.AddView<IMainView, MainWindow>() // View(Transient)
.AddSingletonView<ISplashView, SplashWindow>() // View(Singleton)
.AddViewModel<MainViewModel>() // ViewModel(Transient)
.AddSingletonViewModel<StatusViewModel>() // ViewModel(Singleton)
.AddViewWithViewModel<SettingsView, SettingsViewModel>(); // 配對註冊

4.2 App.xaml.cs 啟動模式

protected override async void OnStartup(StartupEventArgs e)
{
// 1. Pre-DI(License、Serilog bootstrap)
// 2. 背景初始化(Host.CreateDefaultBuilder、DB Migration)
// 3. DI 註冊(Services、ViewModels、Views)
// 4. LocalizationServiceLocator.Initialize(Services)
// 5. UI 建立(MainWindow + MainViewModel + DataContext)
// 6. 延遲任務(更新檢查、背景服務)
}

5. View 基底類別

5.1 WindowBase

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

// 訊息對話框
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 衍生類別

類別用途
MainWindowBase主視窗
DialogWindowBase對話框視窗

6. 服務介面

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; // 可取消
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);

跨 ViewModel 通訊,解耦合。

7. 驗證模式

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);
}

WPF 版 WpfValidatableViewModelBase 會自動 marshal 錯誤通知到 UI Thread。

8. 值轉換器

8.1 內建轉換器

轉換器用途
BooleanToVisibilityConverterbool → Visible/Collapsed
InverseBooleanToVisibilityConverterbool → Collapsed/Visible
NullToVisibilityConverternull → Collapsed

8.2 自訂轉換器範例

[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 安全摘要

情境位置模式
背景 Thread 更新 PropertyWpfViewModelBaseOnPropertyChanged 自動 Dispatch
UI Thread 檢查WindowBaseDispatcher.CheckAccess()
長時間非同步操作ExecuteWithBusyAsync()Task-based,設定 IsBusy
事件訂閱ViewModel 建構子Cleanup() 中取消訂閱
DispatcherTimerUI Thread 定時器建構子中啟動

10. 開發規範

10.1 ViewModel 設計

  • 所有依賴透過建構子注入
  • 使用 [ObservableProperty] 取代手動 SetProperty()
  • 使用 [RelayCommand] 取代手動 new RelayCommand()
  • 提供 Cleanup() 方法取消事件訂閱

10.2 View 設計

  • View 不包含商業邏輯
  • DataContext 透過 DI 或 ViewFactory 設定
  • UI 文字一律使用 {l:L key} Localization

10.3 非同步操作

  • 使用 AsyncRelayCommand(防止重複執行)
  • 長時間操作使用 ExecuteWithBusyAsync()(顯示 Busy 狀態)
  • 避免 async void(僅限事件處理器)

文件版本:v1.0 | 建立日期:2026-03-16 | 對應 Issue:GST-166