メインコンテンツまでスキップ

核心概念詳解

本頁深入說明 ReactiveUI 的五大核心概念,每個都附上設備控制場景範例。


概念關係總覽

ReactiveUI 的核心概念全部圍繞「屬性變化 → IObservable<T> → 綁定 / 訂閱」這條軸線展開。下圖示意 5 大 API 在這條軸線上的位置,以及它們如何透過 WhenAnyValue / WhenAnyObservable 與 Rx.NET 接通:

各概念與圖上節點對應:

概念圖上節點主要 API何時用
ReactiveObjectReactiveObjectRaiseAndSetIfChanged / [Reactive]任何 ViewModel
WhenAnyValueWhenAnyValuethis.WhenAnyValue(x => x.Prop)把屬性變化轉成 IObservable<T>
WhenAnyObservableWhenAnyObservablethis.WhenAnyObservable(x => x.Stream)屬性本身是 IObservable<T>,要取值序列
ReactiveCommandReactiveCommand + Command ObservablesReactiveCommand.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 實作了 INotifyPropertyChangedINotifyPropertyChanging,是所有 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]
實作方式編譯時期 AOPSource Generator(編譯時期產生原始碼)
基底類別任意(Metalama 注入 INPC)必須繼承 ReactiveObject
Observable 整合WhenAnyValue 原生支援
適用場景通用 INPCReactiveUI 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 強大得多:

  1. CanExecute 由 Observable 驅動——屬性變化時自動更新,不需手動呼叫 NotifyCanExecuteChanged
  2. 執行中自動 disable——非同步命令執行期間,按鈕自動變灰
  3. IsExecuting 是 Observable——可以綁定 ProgressRing
  4. ThrownExceptions 是 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 切走時自動停止

延伸閱讀