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}Changed | — | Property 變更 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 內建轉換器
| 轉換器 | 用途 |
|---|---|
BooleanToVisibilityConverter | bool → Visible/Collapsed |
InverseBooleanToVisibilityConverter | bool → Collapsed/Visible |
NullToVisibilityConverter | null → 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 更新 Property | WpfViewModelBase | OnPropertyChanged 自動 Dispatch |
| UI Thread 檢查 | WindowBase | Dispatcher.CheckAccess() |
| 長時間非同步操作 | ExecuteWithBusyAsync() | Task-based,設定 IsBusy |
| 事件訂閱 | ViewModel 建構子 | Cleanup() 中取消訂閱 |
| DispatcherTimer | UI 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