Reactive UI -- 反應式程式設計UI框架入門學習(一)

2022-08-08 12:02:36

推薦一個反應式程式設計的MVVM跨平臺框架。

反應式程式設計

反應式程式設計是一種相對於命令式的程式設計正規化,由函數式的組合宣告來構建非同步資料流。要理解這個概念,可以簡單的藉助Excel中的單元格函數。

 

上圖中,A1=B1+C1,無論B1和C1中的資料怎麼變化,A1中的值都會自動變化,這其中就蘊含了反應式/響應式程式設計的思想。

反應式程式設計對於資料的處理不關心具體的資料值是多少,只要構建出資料的函數式處理,就能並行的非同步處理資料流。

Reactive UI

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框架