推薦一個反應式程式設計的MVVM跨平臺框架。
反應式程式設計是一種相對於命令式的程式設計正規化,由函數式的組合宣告來構建非同步資料流。要理解這個概念,可以簡單的藉助Excel中的單元格函數。
上圖中,A1=B1+C1,無論B1和C1中的資料怎麼變化,A1中的值都會自動變化,這其中就蘊含了反應式/響應式程式設計的思想。
反應式程式設計對於資料的處理不關心具體的資料值是多少,只要構建出資料的函數式處理,就能並行的非同步處理資料流。
Reactive UI 是一種反應式程式設計的跨平臺MVVM框架,支援Xamarin Forms、Xamarin.iOS、Xamarin.Android、Xamarin.Mac、Tizen、Windows Forms、WPF 和UWP。
本文對比經典的MVVM框架MVVMLight框架來展示ReactiveUI框架的特殊之處。
在MVVMLight中,依賴屬性和命令的繫結一般都是放在Xaml中,並且大部分情況下不需要給控制元件定義Name屬性。
<Button Content="{Binding Content}" Command="{Binding OpenFileCommand}"/>
這是屬於弱繫結,在Reactive UI框架中也提供這樣的弱繫結,但Reactive UI框架官方推薦使用後臺強繫結方式。
在強繫結方式中,需要給控制元件定義他的Name屬性。
<Button Name="btnOpenFile"/>
在介面後臺的cs檔案中使用強繫結方式。
//BtnContent是ViewModel中的屬性,btnOpenFile是介面中的控制元件,並指定控制元件需要繫結的依賴屬性 this.OneWayBind(ViewModel, vm => vm.BtnContent, vw => vw.btnOpenFile.Content);
在Reactive UI框架中,提供了單向繫結和雙向繫結兩種繫結型別,上述程式碼中的OneWayBind是屬於ViewModel->View的單向繫結,另外還有一個API Bind則是雙向繫結。
this.Bind(ViewModel, vm => vm.BtnContent, vw => vw.btnOpenFile.Content);
之所以官方推薦這樣的繫結方式,是因為框架中提供了一個管理ViewModel生命週期的API WhenActivated,解決了Xaml弱繫結方式帶來的記憶體洩露的可能性。
在WhenActivated API的函數回撥中進行繫結屬性和Command,可以同步跟蹤View和對應繫結屬性的生命週期,避免發生記憶體洩露。
this.WhenActivated(dispos => { this.OneWayBind(ViewModel, vm => vm.BtnContent, vw => vw.btnOpenFile.Content).DisposeWith(dispos); });
WhenActivated 會在View被啟用時同步呼叫註冊的回撥函數,注意,在OneWayBind後面新增了一個API呼叫DisposeWith,他可以確保當介面被銷燬時,對應的viewModel及其繫結的屬性和命令也會被銷燬。
類似的,繫結Commond
this.WhenActivated(dispos => { this.OneWayBind(ViewModel, vm => vm.BtnContent, vw => vw.btnOpenFile.Content).DisposeWith(dispos); this.BindCommand(ViewModel, viewModel => viewModel.OpenPage, view => view.openButton) .DisposeWith(disposableRegistration); });
這樣的強繫結相比於Xaml中的弱繫結,會有以下的優勢:
1.提供了ViewModel的生命週期管理,避免記憶體洩露。
2.控制元件和後臺屬性的對應關係更為直觀,提高程式碼的可閱讀性。
當然也有一定的缺陷,會增加程式碼量,並且增加View和ViewModel的耦合性。
在MVVMLight中定義一個帶通知的屬性和Commond:
private string content ; public string Content { get { return content; } set { content = value; RaisePropertyChanged(() => Content); } } private RelayCommand openFileCommand = null; public RelayCommand OpenFileCommand { get { return openFileCommand = openFileCommand ?? new RelayCommand(OpenFile); } }
在ReactiveUI中也通成功了類似RaisePropertyChanged和RelayCommand功能的API,RaiseAndSetIfChanged和ReactiveCommand。
private string content; public string Content { get { return content; } set { this.RaiseAndSetIfChanged(ref content,value); } } private ReactiveCommand<Unit, Unit> openFileCommand; public ReactiveCommand<Unit, Unit> OpenFileCommand { get { return openFileCommand = openFileCommand ?? ReactiveCommand.Create(() => { }); } }
其中ReactiveCommand的兩個Unit,前一個是傳入引數,後一個是返回值。ReactiveCommand的定義與MVVMLight大同小異。
但是在ReactiveUI中,還有更簡單方便的定義可通知的屬性,使用標記[Reactive]。
[Reactive] public string Content { get; set; }
以上程式碼等價於
private string content; public string Content { get { return content; } set { this.RaiseAndSetIfChanged(ref content,value); } }
在.Net中,帶通知功能的資料集合一般使用ObservableCollection,但是這個類存在一個限制,不支援多執行緒操作元素,只能在主執行緒中增加或者刪除元素。所以在多執行緒操作ObservableCollection的時候,一般都需要通過Dispatcher或者執行緒上下文來推播操作到UI執行緒。
針對這個問題,ReativeUI框架提供了更優雅的操作方式,SourceList,SourceCache, ObservableCollectionExtended,都是執行緒安全的集合,需要和ReadOnlyObservableCollection一起搭配使用,用於建立可繫結的執行緒安全的資料集合。
//這是用於View繫結的資料集合 private readonly ReadOnlyObservableCollection<string> _disks; public ReadOnlyObservableCollection<string> Disks => _disks; //這裡的ObservableCollectionExtended和SourceList作用相同,都是與_disks強關聯並創 //建副本集合,在運算元據的時候,不直接操作_disks或者Disks,而是對DisksSource或 //DisksSource2進行操作,會自動的同步到_disk集合並更新到繫結的UI,而Disks用於介面繫結。
public ObservableCollectionExtended<string> DisksSource; public SourceList<string> DisksSource2;
//以下程式碼是將DiskSource和DiskSource2與_disk建立強關聯關係的兩種方式 DisksSource = new(); DisksSource.ToObservableChangeSet() .Bind(out _disks) .Subscribe(); DisksSource2 = new SourceList<string>(); DisksSource2.Connect().Bind(out _disks).Subscribe();
以一個讀取磁碟資料夾資訊的小功能為例。
一般都需要定義一個ObservableCollection的Model集合,在子執行緒中需要通過Dispatcher操作集合。
public ObservableCollection<FolderModel> FolderModels { get; set; } private async Task LoadFolderInfoWithSelectedDiskChanged(string diskName) { 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; _dispatcher.Invoke(() => { FolderModels.Add(folderModel); }); } }); }
而在ReactiveUI 框架中,不需要Dispatcher這個東西,而是需要通過一個輔助類ObservableAsPropertyHelper。
ObservableAsPropertyHelper 是一個簡化 IObservable 和 ViewModel 上的屬性之間的互操作的類,為一個普通屬性/欄位和一個IObservable物件之間建立觀察者模式的聯絡。
以上程式碼可以修改成:
//當前選中的磁碟符號,是一個IObservable物件
[Reactive]
public string SelectedDisk { get; set; }
//使用ObservableAsPropertyHelper包裝
private readonly ObservableAsPropertyHelper<IEnumerable<FolderModel>> _folderModels; //FolderModels可用於強繫結
public IEnumerable<FolderModel> FolderModels => _folderModels.Value;
//將_folderModels和SelectedDisk建立觀察者和被觀察者聯絡,構建函陣列合式宣告,當SelectedDisk改變時,
//會自動觸發所註冊的事件並自動給指定的屬性FolderModels賦值。
_folderModels = this.WhenAnyValue(s => s.SelectedDisk) .Where(s => !string.IsNullOrWhiteSpace(s)) .SelectMany(LoadFolderInfoWithSelectedDiskChanged) .ObserveOn(RxApp.MainThreadScheduler) .ToProperty(this, nameof(FolderModels));//將計算後得到的結果賦值到指定的屬性中 private async Task<IEnumerable<FolderModel>> LoadFolderInfoWithSelectedDiskChanged(string diskName) { List<FolderModel> folderModels = new List<FolderModel>(); 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); }
//這個方法中不需要操作FolderModels 只需要把結果返回即可 await Task.CompletedTask; return folderModels; }
其中ObservableAsPropertyHelper包裝的物件是可以任何物件,而LoadFolderInfoWithSelectedDiskChanged方法必須要帶有結果返回的非同步方法,這樣就構成了函數式宣告的非同步資料流。
本文列了一些ReactiveUI的簡單使用,下一篇會通過一個範例程式碼進一步學習ReactiveUI框架