前文Reactive UI -- 反應式程式設計UI框架入門學習(一) 介紹了反應式程式設計的概念和跨平臺ReactiveUI框架的簡單應用。
本文通過一個簡單的小應用更進一步學習ReactiveUI框架的使用和整體佈局,並對比與MVVMLight的不同之處。
應用的功能很簡單,讀取本地計算機的所有碟符,並通過選定碟符展示該碟符下的所有資料夾的名稱和建立時間。
首先新建一個工程,本文使用的是.Net6.0,並新增兩個Nuget包:ReactiveUI.WPF,ReactiveUI.Fody。
ReactiveUI.WPF是框架的核心程式碼包,而ReactiveUI.Fody是一個擴充套件包,像[Reactive]這樣的標記就是在這個包中定義的。
在MVVMLight框架中,View繫結ViewModel需要通過DataContext來繫結在Locator中定義的ViewModel,而在ReactiveUI框架中,則是通過繼承泛型視窗類ReactiveWindow或者泛型使用者控制元件類ReactiveUserControl來自動繫結ViewModel。
<reactiveui:ReactiveWindow x:TypeArguments="local:MainWindowViewModel" x:Class="Calculateor.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Calculateor" xmlns:reactiveui="http://reactiveui.net" mc:Ignorable="d" Title="MainWindow" Height="300" Width="500"> <Grid Margin="10"> <Grid.RowDefinitions> <RowDefinition Height="20"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <ComboBox Name="cmbDisks"> <ComboBox.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding }"/> </DataTemplate> </ComboBox.ItemTemplate> </ComboBox> <ListBox Grid.Row="1" x:Name="lbFolders"></ListBox> </Grid> </reactiveui:ReactiveWindow>
注意以上Xaml程式碼中沒有出現DataContext。
CS檔案中強繫結:
public partial class MainWindow : ReactiveWindow<MainWindowViewModel> { public MainWindow() { InitializeComponent(); ViewModel = new MainWindowViewModel(); this.WhenActivated(dispos => { this.OneWayBind(ViewModel, vm=>vm.Disks, vw=>vw.cmbDisks.ItemsSource) .DisposeWith(dispos); this.Bind(ViewModel, vm => vm.SelectedDisk, vw => vw.cmbDisks.SelectedItem) .DisposeWith(dispos); this.OneWayBind(ViewModel,vm=>vm.FolderModels, vw=>vw.lbFolders.ItemsSource) .DisposeWith(dispos); }); } }
View通過繼承指定為MainWindowViewModel型別的ReactiveWindow,便建立了View和ViewModel之間的關聯,而不需要額外的指定DataContext去繫結。
介面頂部是一個下拉框,用於顯示碟符資訊,ItemSource繫結了ReadOnlyObservableCollection<string>型別物件。
private readonly ReadOnlyObservableCollection<string> _disks; public ReadOnlyObservableCollection<string> Disks => _disks;
其選中的碟符則繫結到了一個string型別的屬性上。注意Reactive標記
[Reactive] public string SelectedDisk { get; set; }
接著用一個ListBox展示具體的資料夾資訊,定義一個FolderModel型別的類來約定需要展示的資訊。
public class FolderModel { public string FolderName { get; set; } public DateTime CreateTime { get; set; } }
ItemSoruce繫結到一個IEnumerable<FolderModel> FolderModels型別上
private readonly ObservableAsPropertyHelper<IEnumerable<FolderModel>> _folderModels; public IEnumerable<FolderModel> FolderModels => _folderModels.Value;
而 ObservableAsPropertyHelper<IEnumerable<FolderModel>> _folderModels則是用來與SelectedDisk建立觀察者模式的聯絡,每次SelectDisk的值改變時,就會觸發方法LoadFolderInfoWithSelectedDiskChanged,並將返回結果賦值到FolderModels物件,最終傳導到UI上。
_folderModels = this.WhenAnyValue(s => s.SelectedDisk) .Where(s => !string.IsNullOrWhiteSpace(s)) .SelectMany(LoadFolderInfoWithSelectedDiskChanged) .ObserveOn(RxApp.MainThreadScheduler)//執行緒排程,後續的程式碼會在主執行緒上呼叫 .ToProperty(this, nameof(FolderModels));
這裡的WhenAnyValue是構建函數宣告的核心API,一般都是與ReactiveUI框架擴充套件的Linq方法搭配使用,前文有過簡單的介紹。
在MVVMLight框架中,ViewModel繼承的是ViewModelBase/ObservableObject,而在ReactiveUI框架中,ViewModel繼承的是ReactiveObject
以下為完整的MainWindowViewModel檔案:
public class MainWindowViewModel : ReactiveObject { public MainWindowViewModel() { DisksSource = new(); DisksSource.ToObservableChangeSet() .Bind(out _disks) .Subscribe(); _folderModels = this.WhenAnyValue(s => s.SelectedDisk) .Where(s => !string.IsNullOrWhiteSpace(s)) .SelectMany(LoadFolderInfoWithSelectedDiskChanged) .ObserveOn(RxApp.MainThreadScheduler) .ToProperty(this, nameof(FolderModels)); Task _ = LoadDisksIqLocal(); } private readonly ReadOnlyObservableCollection<string> _disks; public ReadOnlyObservableCollection<string> Disks => _disks; public ObservableCollectionExtended<string> DisksSource{get;private set;} private readonly ObservableAsPropertyHelper<IEnumerable<FolderModel>> _folderModels; public IEnumerable<FolderModel> FolderModels => _folderModels.Value; [Reactive] public string SelectedDisk { get; set; }
//通過WMI讀取本地計算機的所有磁碟的碟符 private async Task LoadDisksIqLocal() { await Task.Run(() => { ManagementObjectSearcher query = new("SELECT * From Win32_LogicalDisk"); var queryCollection = query.Get(); foreach (var item in queryCollection) { var diriveType = (DriveType)int.Parse(item["DriveType"].ToString()); if (diriveType == DriveType.Fixed) { var diskID = item["DeviceID"].ToString(); DisksSource.Add(diskID); } } }); } private async Task<IEnumerable<FolderModel>> LoadFolderInfoWithSelectedDiskChanged(string diskName) { List<FolderModel> folderModels = new List<FolderModel>(); await Task.Run(() => { var files = Directory.GetDirectories(diskName); foreach (var fileName in files) { FolderModel folderModel = new FolderModel(); DirectoryInfo directoryInfo = new DirectoryInfo(fileName); folderModel.FolderName = directoryInfo.Name; folderModel.CreateTime = directoryInfo.CreationTime; folderModels.Add(folderModel); } }); return folderModels; } }
下面需要定義ListBox資訊需要以怎樣的格式來展示。一般的常規做法是通過Style來客製化控制元件的模板展示客製化化的資料格式,而在ReactiveUI框架中,還有其他的選擇。
在ReactiveUI中,會根據ListBox ItemSource所繫結的集合型別來自動的搜尋這個型別所關聯的UserControl來作為ListBox的模板。
簡單的說,只需要給上文中的FolderModel指定一個UserControl即可,而不需要額外的指定Style或者Template。
所以View中的ListBox程式碼很簡單:
<ListBox Grid.Row="1" x:Name="lbFolders"></ListBox>
新增一個UserControl的類FolderInfoUC.xaml與FolderModel繫結:
<reactiveui:ReactiveUserControl x:Class="Calculateor.FolderInfoUC" x:TypeArguments="local:FolderModel" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:Calculateor" xmlns:reactiveui="http://reactiveui.net" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <UniformGrid Columns="2"> <TextBlock Text="{Binding FolderName}" HorizontalAlignment="Left"/> <TextBlock Text="{Binding CreateTime}" HorizontalAlignment="Right"/> </UniformGrid> </reactiveui:ReactiveUserControl>
這裡的TextBlock控制元件除了展示資料之外沒有其他用途,所以直接使用了Xaml的繫結方式,而View通過ReactiveUserControl來指定他的ViewModel型別為FolderModel,這樣就建立了FolderModel和FolderInfoUC之間的聯絡。
當然,在很多情況下處理複雜的高度自定義的資料展示時,還是需要Style的配合。
需要注意的是,這裡的FolderModel資料型別本身比較簡單,不需要繼承自ReactiveObject。
還有一個情況需要注意,如主介面上的下拉框Combobox。這個控制元件繫結的是一個簡單的string型別的集合 ReadOnlyObservableCollection<string>,不推薦為CLR中的基礎型別關聯UserControl,所以需要Xaml中指定ItemTemplate,否則無法顯示資料。
截至本文,ReactiveUI相比於MVVMLight框架,有以下的不同點:
1.ReactiveUI推薦強繫結,並提供了管理ViewModel和屬性的生命週期的方法。
2.易於構建響應式的可觀察的函數宣告式的資料流處理。
3.簡化了ViewModel和View之間繫結的操作方式,並強化了兩者之間的聯絡貫穿在整個應用的生命週期中。
4.擴充套件了動態資料集合在多執行緒下的操作,提供執行緒安全的可繫結動態集合。
本文以一個小應用簡單介紹了ReactiveUI整體框架的使用,其中一些核心的API WhenAnyValue、ObservableAsPropertyHelper、ObservableCollectionExtended等沒有詳細展開,後續會對這些API的高階應用有更深入的學習和了解,學習和閱讀ReactiveUI的原始碼。
git地址:https://github.com/reactiveui/reactiveui
官網地址:https://www.reactiveui.net/