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

2022-08-09 18:00:59

前文Reactive UI -- 反應式程式設計UI框架入門學習(一)  介紹了反應式程式設計的概念和跨平臺ReactiveUI框架的簡單應用。

本文通過一個簡單的小應用更進一步學習ReactiveUI框架的使用和整體佈局,並對比與MVVMLight的不同之處。

應用的功能很簡單,讀取本地計算機的所有碟符,並通過選定碟符展示該碟符下的所有資料夾的名稱和建立時間。

 

首先新建一個工程,本文使用的是.Net6.0,並新增兩個Nuget包:ReactiveUI.WPF,ReactiveUI.Fody

ReactiveUI.WPF是框架的核心程式碼包,而ReactiveUI.Fody是一個擴充套件包,像[Reactive]這樣的標記就是在這個包中定義的。

繫結ViewModel

在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,這樣就建立了FolderModelFolderInfoUC之間的聯絡。

當然,在很多情況下處理複雜的高度自定義的資料展示時,還是需要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/