核心概念詳解
本頁深入說明 ReactiveUI 的五大核心概念,每個都附上設備控制場景範例。
概念關係總覽
ReactiveUI 的核心概念全部圍繞「屬性變化 → IObservable<T> → 綁定 / 訂閱」這條軸線展開。下圖示意 5 大 API 在這條軸線上的位置,以及它們如何透過 WhenAnyValue / WhenAnyObservable 與 Rx.NET 接通:
各概念與圖上節點對應:
| 概念 | 圖上節點 | 主要 API | 何時用 |
|---|---|---|---|
| ReactiveObject | ReactiveObject | RaiseAndSetIfChanged / [Reactive] | 任何 ViewModel |
| WhenAnyValue | WhenAnyValue | this.WhenAnyValue(x => x.Prop) | 把屬性變化轉成 IObservable<T> |
| WhenAnyObservable | WhenAnyObservable | this.WhenAnyObservable(x => x.Stream) | 屬性本身是 IObservable<T>,要取值序列 |
| ReactiveCommand | ReactiveCommand + Command Observables | ReactiveCommand.CreateFromTask(...) | 響應式命令;自帶 canExecute / IsExecuting / ThrownExceptions |
| ObservableAsPropertyHelper (OAPH) | ObservableAsPropertyHelper | .ToProperty(...) / [ObservableAsProperty] | 把任意 IObservable<T> 轉成唯讀的 INPC 屬性 |
讀圖重點:
- 任何屬性變化都必須先穿過
WhenAnyValue/WhenAnyObservable/ Command Observable 這層橋接 API 才能進入 Rx.NET 領域,然後才能套Where/Throttle/Select等 operator。 - View 端有 3 種終點:
Bind/OneWayBind(與 XAML / 控制項雙向或單向綁定)、純Subscribe(自訂行為,例如顯示 MessageBox、播放音效)。後者必須包在WhenActivated內並DisposeWith以避免記憶體洩漏。
ReactiveObject — ViewModel 基底類別
ReactiveObject 實作了 INotifyPropertyChanged 和 INotifyPropertyChanging,是所有 ReactiveUI ViewModel 的基底。
RaiseAndSetIfChanged
標準的屬性宣告方式——值不同才觸發通知:
public class DeviceSettingsViewModel : ReactiveObject
{
private string _deviceName = "";
public string DeviceName
{
get => _deviceName;
set => this.RaiseAndSetIfChanged(ref _deviceName, value);
}
private int _pollingInterval = 500;
public int PollingInterval
{
get => _pollingInterval;
set => this.RaiseAndSetIfChanged(ref _pollingInterval, value);
}
private bool _isAutoReconnect = true;
public bool IsAutoReconnect
{
get => _isAutoReconnect;
set => this.RaiseAndSetIfChanged(ref _isAutoReconnect, value);
}
}
搭配 ReactiveUI.SourceGenerators 簡化
安裝 ReactiveUI.SourceGenerators 後,可用 [Reactive] 屬性大幅減少樣板程式碼(編譯時期自動產生 getter/setter + PropertyChanged):
public partial class DeviceSettingsViewModel : ReactiveObject
{
[Reactive] public string DeviceName { get; set; } = "";
[Reactive] public int PollingInterval { get; set; } = 500;
[Reactive] public bool IsAutoReconnect { get; set; } = true;
}
與 Metalama [Observable] 的比較
公司的 Metalama 也提供 [Observable] Aspect 來自動實作 INPC。差異在於:
| 項目 | Metalama [Observable] | ReactiveUI [Reactive] |
|---|---|---|
| 實作方式 | 編譯時期 AOP | Source Generator(編譯時期產生原始碼) |
| 基底類別 | 任意(Metalama 注入 INPC) | 必須繼承 ReactiveObject |
| Observable 整合 | 無 | WhenAnyValue 原生支援 |
| 適用場景 | 通用 INPC | ReactiveUI MVVM |
WhenAnyValue — 監聽屬性變化
WhenAnyValue 是 ReactiveUI 的核心——它將屬性變化轉換為 IObservable<T>,讓你能用 Rx.NET 的所有 operators 來處理。
監聽單一屬性
// 溫度超過 80°C 時觸發警報
this.WhenAnyValue(x => x.Temperature)
.Where(temp => temp > 80.0)
.Throttle(TimeSpan.FromSeconds(1))
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(temp => TriggerAlarm($"溫度過高: {temp}°C"));
監聽多個屬性
// 組合 IP 和 Port,任一變化時更新連線字串
this.WhenAnyValue(
x => x.DeviceIp,
x => x.DevicePort,
(ip, port) => $"{ip}:{port}")
.Subscribe(addr => ConnectionString = addr);
衍生狀態
// 根據多個條件計算設備是否可啟動
this.WhenAnyValue(
x => x.IsConnected,
x => x.IsCalibrated,
x => x.HasRecipe,
(connected, calibrated, hasRecipe) => connected && calibrated && hasRecipe)
.ToProperty(this, x => x.CanStartProcess, out _canStartProcess);
與 Rx.NET operators 的組合
因為 WhenAnyValue 回傳的是標準 IObservable<T>,你可以用所有 Rx.NET operators:
// 使用者輸入搜尋文字 → 去抖動 → 搜尋
this.WhenAnyValue(x => x.SearchText)
.Throttle(TimeSpan.FromMilliseconds(300))
.DistinctUntilChanged()
.Where(text => text?.Length >= 2)
.SelectMany(text => SearchDevicesAsync(text))
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(results => SearchResults = results);
ReactiveCommand — 響應式命令
ReactiveCommand 比傳統的 RelayCommand / DelegateCommand 強大得多:
- CanExecute 由 Observable 驅動——屬性變化時自動更新,不需手動呼叫
NotifyCanExecuteChanged - 執行中自動 disable——非同步命令執行期間,按鈕自動變灰
IsExecuting是 Observable——可以綁定 ProgressRingThrownExceptions是 Observable——錯誤處理集中管理
基本:同步命令
ResetCommand = ReactiveCommand.Create(() =>
{
Temperature = 0;
Pressure = 0;
Status = "已重置";
});
非同步命令
// 建立非同步命令,執行期間按鈕自動 disable
ConnectCommand = ReactiveCommand.CreateFromTask(async () =>
{
ConnectionStatus = "連線中...";
await deviceClient.ConnectAsync(DeviceIp, DevicePort);
ConnectionStatus = "已連線";
});
CanExecute 自動化
// CanExecute:已連線且未執行中才能啟動
var canStart = this.WhenAnyValue(
x => x.IsConnected,
x => x.IsRunning,
(connected, running) => connected && !running);
StartCommand = ReactiveCommand.CreateFromTask(
StartProcessAsync,
canStart); // 屬性變化時自動更新按鈕狀態
IsExecuting 綁定 ProgressRing
// ViewModel
StartCommand.IsExecuting
.ToProperty(this, x => x.IsBusy, out _isBusy);
<!-- XAML -->
<mah:ProgressRing IsActive="{Binding IsBusy}" />
ThrownExceptions 集中處理
ConnectCommand.ThrownExceptions
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(ex =>
{
ConnectionStatus = "連線失敗";
Logger.Error($"連線錯誤: {ex.Message}");
});
帶參數的命令
// 接收設備 ID 作為參數
SelectDeviceCommand = ReactiveCommand.CreateFromTask<string>(async deviceId =>
{
var device = await LoadDeviceAsync(deviceId);
SelectedDevice = device;
});
ObservableAsPropertyHelper (OAPH) — Observable → 唯讀屬性
OAPH 將任何 IObservable<T> 轉換為唯讀的 ViewModel 屬性,搭配 INPC 通知。這是把設備資料流直接接到 UI 的關鍵。
基本用法
private readonly ObservableAsPropertyHelper<double> _currentTemp;
public double CurrentTemp => _currentTemp.Value;
public DeviceMonitorViewModel(IObservable<DeviceData> deviceStream)
{
_currentTemp = deviceStream
.Select(d => d.Temperature)
.ToProperty(this, x => x.CurrentTemp);
}
搭配 [ObservableAsProperty] SourceGenerators
public class DeviceMonitorViewModel : ReactiveObject
{
[ObservableAsProperty] public double CurrentTemp { get; }
[ObservableAsProperty] public double CurrentPressure { get; }
[ObservableAsProperty] public string DeviceStatus { get; }
public DeviceMonitorViewModel(IObservable<DeviceData> deviceStream)
{
deviceStream
.Select(d => d.Temperature)
.ToPropertyEx(this, x => x.CurrentTemp);
deviceStream
.Select(d => d.Pressure)
.ToPropertyEx(this, x => x.CurrentPressure);
deviceStream
.Select(d => d.Status.ToString())
.ToPropertyEx(this, x => x.DeviceStatus);
}
}
完整設備監控範例
public class SensorDashboardViewModel : ReactiveObject
{
[ObservableAsProperty] public double Temperature { get; }
[ObservableAsProperty] public double Pressure { get; }
[ObservableAsProperty] public bool IsAlarm { get; }
[ObservableAsProperty] public string LastUpdate { get; }
public SensorDashboardViewModel(IObservable<SensorReading> sensorStream)
{
var shared = sensorStream
.ObserveOn(RxApp.MainThreadScheduler)
.Publish()
.RefCount();
shared.Select(s => s.Temperature)
.ToPropertyEx(this, x => x.Temperature);
shared.Select(s => s.Pressure)
.ToPropertyEx(this, x => x.Pressure);
shared.Select(s => s.Temperature > 80 || s.Pressure > 5.0)
.DistinctUntilChanged()
.ToPropertyEx(this, x => x.IsAlarm);
shared.Select(_ => DateTime.Now.ToString("HH:mm:ss"))
.ToPropertyEx(this, x => x.LastUpdate);
}
}
WhenActivated — 生命週期管理
WhenActivated 解決了 ReactiveUI 中最重要的問題:何時建立訂閱、何時清理。
當 View 出現在畫面上時啟動訂閱,離開時自動 Dispose——不需要手動管理 CompositeDisposable。
在 ViewModel 中使用
public class DeviceMonitorViewModel : ReactiveObject, IActivatableViewModel
{
public ViewModelActivator Activator { get; } = new();
[Reactive] public double Temperature { get; set; }
public DeviceMonitorViewModel(IDeviceService deviceService)
{
this.WhenActivated(disposables =>
{
// 這些訂閱在 View 出現時建立,離開時自動 Dispose
deviceService.GetTemperatureStream()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(temp => Temperature = temp)
.DisposeWith(disposables);
deviceService.GetAlarmStream()
.Subscribe(alarm => HandleAlarm(alarm))
.DisposeWith(disposables);
// View 離開時的清理
Disposable.Create(() => Logger.Info("監控停止"))
.DisposeWith(disposables);
});
}
}
在 View 中使用
public partial class DeviceMonitorView : ReactiveUserControl<DeviceMonitorViewModel>
{
public DeviceMonitorView()
{
InitializeComponent();
this.WhenActivated(disposables =>
{
// 響應式綁定(Type-safe,不怕 typo)
this.Bind(ViewModel,
vm => vm.DeviceIp,
v => v.IpTextBox.Text)
.DisposeWith(disposables);
this.BindCommand(ViewModel,
vm => vm.ConnectCommand,
v => v.ConnectButton)
.DisposeWith(disposables);
this.OneWayBind(ViewModel,
vm => vm.Temperature,
v => v.TempLabel.Content,
temp => $"{temp:F1}°C")
.DisposeWith(disposables);
});
}
}
為什麼需要 WhenActivated
| 沒有 WhenActivated | 有 WhenActivated |
|---|---|
| 訂閱在建構子建立,ViewModel 存活就一直跑 | 訂閱隨 View 出現/消失 |
| 離開頁面後訂閱仍在,浪費資源 | 離開頁面自動 Dispose |
| 需要手動管理 CompositeDisposable | 框架自動管理 |
| Tab 切換時設備輪詢不會停 | Tab 切走時自動停止 |
延伸閱讀:
- 公司場景 Pattern — 以上概念的實際應用
- 遷移指南 — 從 CommunityToolkit.Mvvm 遷移
- Rx.NET 常用 Operators — WhenAnyValue 背後的 Rx operators