Reactive UI — 反應式編程UI框架入門學習(二)

前文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="//schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="//schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="//schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="//schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Calculateor"
        xmlns:reactiveui="//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="//schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="//schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="//schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="//schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:Calculateor"
              xmlns:reactiveui="//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地址://github.com/reactiveui/reactiveui

官網地址://www.reactiveui.net/