Reactive Extensions入门(5):ReactiveUI MVVM框架
??? 從前面幾篇文章可以了解到,Rx作為LINQ的一種擴展,極大地簡化了異步編程。但Rx的用法不僅如此,由于其可高的擴展性,在其他很多方面也有所應用。
??? 在前面例子中,我們使用代碼和UI界面上的元素打交道,這種方式在傳統的Winfom編程中很常見,但是在基于XAML構造的界面這種應用程序中,這樣顯得不是非常友好,XAML中聲明式編程可以使得程序更加簡潔,傳統的方式沒有利用到XAML中強大的綁定功能。之前,我們大量使用了諸如Observable.FromEvent這樣的操作,然后來使用后臺代碼來設置控件的屬性,這都是傳統的編程方式。
??? 當然,對于規模較小的程序來說,這種方式無可厚非。這種方式的最大的缺點在于,對于測試很不友好,要測試這樣的應用程序很困難,我們需要創建UI控件并模擬輸入,這樣效率不高而且不可靠。另一個缺點是,這種方式使得代碼高度耦合而且脆弱。針對這些問題,一種稱為Model-View-ViewModel(MVVM)的設計模式逐漸發展起來。
??? 結合MVVM模式和Rx類庫,發展出了ReactiveUI這個MVVM框架。他能夠使得應用程序可以管理,并能使用聲明式、函數式的方式來表達復雜的對象間的交互。換句話說,ReactiveUI能夠幫助我們描述屬性之間是如何聯系起來的,即使有些屬性與異步方法調用有關。
?
1. MVVM模式
??? Model-View-ViewModel模式是充分利用XAML設計平臺上的數據綁定功能而產生的一種設計模式。在該模式中,Model是用來表現應用程序的數據以及與界面獨立的邏輯的核心對象。View就是UI控件及界面,比如窗體控件或者用戶自定義控件。值得注意的是對于同一數據對象,可能有多種表現形式,比如對于同一數據源,有的視圖用來顯示統計或者全局的信息,有的顯示每一項的詳細信息。
??? 一般我們很熟悉的是MVC模型,所以MVVM模式中的ViewModel是其特別的地方。從名字上看,ViewModel是一種針對視圖的模型。可能有點不好理解。舉個例子來說:在用戶注冊頁面(View)中,一般有輸入用戶名,密碼,重復輸入密碼這幾個輸入框。在這個視圖中,用戶名和密碼顯然是存在于Model中,但是 重復輸入密碼 這一項并不屬于Model,它顯然不應該存在于真實的數據模型中,該項只是用在View中。
??? 在傳統的以XAML為界面的程序中,開發者一般使用頁面(View)的后臺的代碼來存儲這個重復輸入密碼值,但是這樣同樣存在可測試性和緊耦合的問題。例如,如果我們要測試“只有當密碼和重復密碼輸入的值匹配,提交按鈕才可以使用”這樣一個場景就變得有點困難。現在,我們將這個字段存在另一個稱之為ViewModel的對象中,這個ViewModel對象只是一個普通的類,他并不需要繼承自UI控件,我們可以將該對象看做是與View的交互邏輯。在我們的例子中,驗證兩次密碼是否匹配以及在匹配時讓提交按鈕可用,這些邏輯都應該寫在ViewModel對象中。對于每一個View,都應該有一個對應的ViewModel對象。
?
1.1 ViewModeld的理念
??? MVVM最強大的一方面在于它的目標是將一個命令(command)或者屬性(property)是什么和如何執行分開來。ViewModel是對屬性和命令的一種思考。在傳統的基于Codebehind的用戶交互框架中,開發者需要思考控件的事件和屬性。當以這種方式編寫代碼時,意味著事件和相應的控件緊緊的聯系在一起。使得測試變得困難,因為我們需要模擬出控件的動作才能測試控件對應的事件及功能是否正常。
當使用MVVM的ViewModels時,最重要的是將這兩部分邏輯分開來。在View中決定了這些控件如何被觸發,同時,控件對應的一些屬性利用XAML的綁定技術和ViewModel綁定起來。
?
1.2 MVVM框架的作用
??? 現在有很多開源的MVVM框架可以使用了,如MVVMLight、Prism,這些框架框架各有優點。但是他們都提供了實現MVVM模式的最基本要素。首先,這些框架為ViewModel對象提供了一個基類,當這些對象的屬性在屬性值發生改變時會得到通知,這個是通過實現INotifyPropertyChanged接口來完成的,這個接口很關鍵,因為他通知View需要更新綁定到界面上的數據。MVVM提供了處理命令的一套系統,當用戶發出一些命令時它能夠很好的處理。這是通過實現ICommand接口來實現的,這個接口通常包含在UI控件中。
?
2.ReactiveUI庫
??? ReactiveUI類庫是實現了MVVM模式的框架,他移除了一些Rx和用戶界面進行交互的代碼。ReactiveUI的核心思想是使開發者能夠將屬性變更以及事件轉換為IObservable對象,然后在需要的時候使用IObservable對象將這些對象轉換到屬性中來。他的另一個核心目標是可以在ViewModel中相關屬性發生變化時可以可執行相應的命令。雖然其他的框架也允許這么做,但是ReactiveUI會在依賴屬性變更時自動的去更新結果,而不需要通過拉或者調用類似UpdateTheUI之類的方法。
?
2.1 核心類
ReactiveObject:它是ViewModel對象,該對象實現了INotifyPropertyChanged接口。除此之外,該對象也提供了一個稱之為Changed的IObservable接口,允許其他對象來注冊,從而使得該對象屬性變更時能夠得到通知。使用Rx中強大的操作符,我們還可以追蹤到一些狀態是如何改變的。
ReactiveValidateObject:該對象繼承自ReactiveObject對象,它通過實現IDataErrorInfo接口,利用DataAnnotations來驗證對象。因此屬性的值可以使用一些限制標記,UI界面能夠自動的在屬性的值違反這些限制時顯示出這些錯誤。
ObservableAsPropertyHelper<T>:該類可以很容易的將IObservable對想轉換為一個屬性,該屬性存儲該對象的最新值,并且在屬性值發生改變時能夠觸發NofityPropertyChanged事件。使用該類,我們能夠從IObservable中派生出一些新的屬性。
ReactiveCommand:該類實現了ICommand和IObservable接口,并且當Execute執行時OnNext方法就會被執行。該對象的CanExecute可以通過IObservable<bool>來定義。
ReactiveAsyncCommand:該對象繼承自ReactiveCommand,并且封裝了一種通用的模式。即“觸發一步命令,然后將結果封送到dispather線程中”該對象也允許設置最大并行值。當達到最大值時,CanExecute方法返回false。
?
3.使用ReactiveObject實現ViewModels
和其他MVVM框架一樣,ReactiveUI框架有一個對象來作為ViewModel類。該對象和基于傳統的實現了ViewModel對象的MVVM框架如Foundation,Cliburn.Micro類似。但是最大的不同在于,ReactiveUI能夠很容易的通過名為Changed的IObservable接口注冊事件變化。在任何一個屬性發生變化時,都會觸發通知,客戶端通常只需要關注感興趣的一兩個變化了的屬性。使用ReactiveUI,可以通過WhenAny擴展方法很容易的獲取這些屬性值:
var newLoginVm = new NewUserLoginViewModel();newLoginVm.WhenAny(x => x.User, x => x.Value) .Where(x => x.Name == "Bob") .Subscribe(x => MessageBox.Show("Bob is already a user!"));IObservable<bool> passwordIsValid = newLoginVm.WhenAny(x => x.Password, x => x.PasswordConfirm,(pass, passConf) => (pass.Value == passConf.Value));??? WhenAny語法看起來過有點奇怪。方法中第一個參數是通過匿名方法定義的一系列屬性。在上面的例子中,我們關心的是神馬時候Password或者PasswordConfirm發生變化。最后一個參數和Zip操作符中的類似,他使用一個匿名方法來將兩個結果結合起來,然后返回結果。當這兩個屬性中的任何一個發生變化時,方法就會執行,并以IObservable的形式返回執行結果,在上面的例子中就是passwordIsValid這個對象。
??? 對于ReactiveObject,值得注意的是,屬性必須明確的使用特定的語法進行定義。因為簡單的get,set并沒有實現INotifyPropertyChanged,從而不會通知ReactiveObject對象該屬性發生了改變。唯一例外的就是,如果一個屬性在構造器中初始化了,在以后的程序中不會發生改變。在ReactiveObject中,屬性的命名也需要注意,用作屬性的私有字段必須為屬性名稱前面加上下劃線。下面的例子展示了如何使用ReactiveObject聲明一個可讀寫的屬性。
public class AppViewModel : ReactiveObject {int _SomeProp;public int SomeProp{get { return _SomeProp; }set { this.RaiseAndSetIfChanged(x => x.SomeProp, value); }} }傳統的實現IpropertyChangeNofity接口的實現方法如下:
public class AppViewModel : INotifyPropertyChanged {int _SomeProp;public int SomeProp{get { return _SomeProp; }set{if (_SomeProp == value)return;_SomeProp = value;RaisePropertyChanged("SomeProp");}}public event PropertyChangedEventHandler PropertyChanged;private void RaisePropertyChanged(string propertyName){PropertyChangedEventHandler handler = this.PropertyChanged;if (handler != null){handler(this, new PropertyChangedEventArgs(propertyName));}} }??? WhenAny實現了ReactiveUI的核心功能之一,它使得開發者能夠很容易將相關屬性變化用IObservable表示。該功能使得可以直接使用Rx以聲明的方式創建狀態機。
??? 除了使用Rx來描述復雜的異步操作事件之外,Rx和ReactiveUI結合可以使得對象在某個特定的狀態下可以得到通知,即使這種狀態涉及到多個不同的對象或者屬性。
?
4. ReactiveCommand
ReactiveCommand實現了ICommand接口,他可以模擬簡單的ICommand實現。我們可以將它看做是一種ICommand,可以使用Create靜態方法創建。
var cmd = ReactiveCommand.Create(x => true, x => Console.WriteLine(x)); cmd.CanExecute(null); //方法輸出true cmd.CanExecute("Hello"); //方法輸出"Hello"下面構造了一個Command,該Command只在鼠標松開時觸發。
var mouseIsUp = Observable.Merge(Observable.FromEvent<MouseButtonEventArgs>(window, "MouseDown").Select(_ => false),Observable.FromEvent<MouseButtonEventArgs>(window, "MouseUp").Select(_ => true)).StartWith(true); var cmd = new ReactiveCommand(mouseIsUp); cmd.Subscribe(x => Console.WriteLine(x));??? 上面的例子演示了如何使用IObservable構造Command。通常我們使用WhenAny創建IObservable然后構造Command對象。大多數情況下,只有當特定的屬性被設置或者取消設置時會觸發Command。例如在之前的NewUserLoginViewModel中。
IObservable<bool> passwordIsValid = newLoginVm.WhenAny( x => x.Password, x => x.PasswordConfirm, (pass, passConf) => (pass.Value == passConf.Value)); var confirmCommand = new ReactiveCommand(passwordIsValid);??? View通過按鈕或者菜單綁定confirmCommand,當在兩次密碼不匹配時,按鈕或者菜單就會呈現出灰色。當密碼或者重復密碼輸入框中的值發生變化時,ReactiveCommand就會重新求值,來決定是否使得按鈕或者菜單可用。
??? 值得注意的是,當屬性發生變化時,Command的CanExecute會立即自動更新,而不依賴于CommandManager.RequerySuggested。在WPF或者Silverlight中存在這個bug,除非你切換焦點或者點擊,按鈕不會重新改變其狀態。使用IObservable意味著Commanding框架確切的知道在狀態發生改變時,不需要重新手動執行頁面上的每一個Command對象。
??? ReactiveCommand對象本身可以被注冊,并且在執行Exectue方法時,提供一些有用的信息。這表明,訂閱者可以執行一些Reactive可以執行的一些動作,使得我們能夠更好的進行控制。如下:
var cmd = new ReactiveCommand(); cmd.Where(x => ((Int32)x % 2 == 0)).Subscribe(x => Console.WriteLine("{0} is Even numbers .", x)); cmd.Where(x => ((Int32)x % 2 != 0)).Timestamp().Subscribe(x => Console.WriteLine("{0} is Odd,{1}", x.Value, x.Timestamp));cmd.Execute(2);//輸出“2 is Even numbers. cmd.Execute(3);//輸出 3 is Odd,2012/3/4 20:38:51 +08:00?
4.1使用ObservableAsPropertyHelper將Observables轉化為Properties
??? 使用WhenAny方法,可以監視對象屬性的變化,并針對這些變化生成IObservable對象。但是有時候,我們想將這些生成的IObservable對象設置為一種輸出屬性。想象一下有這樣一個場景,有一個取色器,用戶能夠通過3個Slide分別設置R,G,B值。每一個Slide可以使用ViewModel對象來表示,取值范圍為0到1。為了顯示結果,我們需要將RGB合成為一個XAML顏色對象。當RGB中的任何一個發生變化時,我們需要更新顏色屬性。
??? 我們可以常簡單的通過WhenAny創建一個IObservable<Color>對象,但是我們想將這個值存回到屬性中。ReactiveUI提供了一個稱之為ObservableAsPropertyHelper的對象,該對象可以存儲IObservable中的最新值。為了演示這一操作,我們需要創建一個“輸出屬性”
ObservableAsPropertyHelper<Color> finalColor; public Color FinalColor {get {return finalColor.Value;} }??? 注意到屬性并沒有set方法,這是因為屬性是由IObservable生成的,而不需要手動設定。在ViewModel的構造函數中,我們將描述如何從RGB產生FinalColor:
IObservable<Color> color = this.WhenAny(x => x.Red, x => x.Green, x => x.Blue, (r, g, b) => new Color(r.Value, g.Value, b.Value)); finalColor = color.ToProperty(this, x => x.FinalColor);??? 這一步只需在構造函數中執行一次。現在只要Red,Green,或者Blue中的任何一個發生變化,FinalColor對象都會更新以反映最新的變化值。
??? ReactiveObject和ReactiveCommad是創建ViewModel對象的兩個核心工具。使用它們我們可以使用屬性和命令以及通過描述屬性和命令之間的動態關系來構建一個View。當我們關心狀態變化,以及某一個屬性的變化對另外一個變化產生的影像時時,我們可以將屬性轉換為IObservable對象。這一點可以幫助我們很好地測試ViewModel對象。
ReactiveUI還有一些功能能夠幫助我們在用戶界面上優雅的處理異步方法調用。幾乎大部分的應用程序都需要運行后臺程序,Reactive的靈活方便的異步操作能力使得ReactiveUI在獲取這些異步計算結果時變得很容易。
?
4.2使用ReactiveAsyncCommand處理異步方法調用
??? 在Winform或者WPF應用程序中,如果事件執行需要耗費很長時間,比如讀取一個很大的文件,那么UI很容易卡死。這是因為程序在忙于處理文件讀寫操作或者在等待網絡數據傳輸而不能夠刷新用戶界面。在Silverligh或者Windows Phone中通過規定UI線程不允許阻塞來解決了這一問題。
??? 通常解決這一問題的辦法是另外開一個線程或者使用線程池來處理這些耗時操作,但是這又帶來了第二個問題,那就是所有基于XAML的框架都是線程關聯的(thread affinity),這意味著,我們只能夠從創建該對象的那個線程訪問該對象。所以如果您在非UI線程中更新UI,比如執行完了一些操作后直接進行類似textbox.text=results這類的更新就會拋出錯誤。因為非UI線程不能夠更新UI上的對象。
傳統的解決這一方法是在更新UI操作時調用Dispatcher.BeginInvoke方法,該方法要求代碼在UI線程中運行,大致代碼如下:
void OnSomeUIEvent(object o, EventArgs e) {var someData = this.SomePropertyICanOnlyGetOnTheUIThread;var t = new Task(() => {var result = DoSomethingInTheBackground(someData);Dispatcher.BeginInvoke(new Action(() => {this.UIPropertyThatWantsTheCalculation = result;}));};t.Start(); }??? ReactiveAsyncCommand將這一模式進行了一定的封裝,使得我們編寫代碼更加容易。例如:用戶界面上有時需要某個異步方法在某一段時間運行,在異步方法運行的過程中讓一些按鈕或者控件處于Disable狀態。稍微友好一些的用戶界面在后臺正在進行的操作時給UI界面一些提示,比如在界面上顯示,“程序正在進行xxx……”的提示,這樣顯得更加友好。
??? 由于ReactiveAsyncCommand直接繼承自ReactiveCommand,所以它能做基類的所有功能。使用Execute,使得Command開始在后臺執行時并可以通知用戶。ReactiveAsyncCommand和ReactiveCommand不同之處在于,它內建了能夠自動跟蹤后臺線程中運行的任務的數量。
??? 下面是一個簡單的使用Command的例子,它在后臺線程的Task中運行,并且只運行一次。
var cmd = new ReactiveAsyncCommand(); cmd.RegisterAsyncAction(i => {Thread.Sleep((int)i * 1000); });cmd.Execute(5); cmd.CanExecute(5);//False??? ReactiveAsyncCommand對象中是使用RegisterAsyncAction來注冊異步執行操作的。它能夠注冊異步方法和同步方法,這些方法將會在后臺線程中執行,并返回IObservable數據表示執行結果會在未來的某一時刻到來。IObservale通常對應Command調用。每一次執行Execute方法將會將結果存入到IObservable對象中。
?
4.3構造一個ViewModel例子
??? 講了這麼多ReactiveUI框架的幾個重要對象。現在用一個簡單的View以及與之相關聯的VeiwModel來展示如何使用ViewModel。本例子將展示如何執行一些簡單的和按鈕相關的命令,并模擬在后臺執行一些費時的操作。然后將結果顯示在UI界面上。
??? 首先來看看我們的前臺頁面,也就是View,在這里我建立的是一個簡單的WPF應用程序。
<Window x:Class="RxUI.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"Title="MainWindow" Height="350" Width="525" x:Name="Window"><Grid DataContext="{Binding ViewModel, ElementName=Window}"><StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"><TextBlock Text="{Binding DataFromTheInternet}" FontSize="18"/><Button Content="Click me!" Command="{Binding GetDataFromTheInternet}"CommandParameter="5" MinWidth="75" Margin="0,6,0,0"/></StackPanel></Grid> </Window>??? View中有幾個地方我們需要注意。首先我們將頂級Grid容器的DataContext參數綁定到我們的ViewModel對象上。這樣,當我們使用XAML數據綁定時,這些元素相對于ViewModel而不是View來進行綁定。然后我們定義了一個TextBlock,將其內容綁定到DataFromTheInternet屬性上。最后,我們綁定Button的Command屬性到我們再ViewModel中定義的一個稱之為GetDataFromTheInternet的Command對象上。相關的定義以及ViewModel代碼如下:
public partial class MainWindow : Window {public AppViewModel ViewModel { get; protected set; }public MainWindow(){ViewModel = new AppViewModel();InitializeComponent();} }class AppViewModel:ReactiveObject {ObservableAsPropertyHelper<String> dataFromTheInternet;public string DataFromTheInternet{get { return dataFromTheInternet.Value; }}public ReactiveAsyncCommand GetDataFromTheInternet { get; protected set; } }??? 在View中,我們通過get set方法創建了一個名為ViewModel的普通屬性,然后我們再構造函數的InitializeComponet方法之前初始化了該屬性。接著,我們定義了一個ViewModel類來對我們的View進行建模。通過ObservableAsPropertyHelper以及ReactiveAsyncCommand定義了一個輸出屬性。必須是屬性才在XAML中綁定,屬性的setter是Protected的,因為我們只需要在構造函數中進行實例化,之后就不會再對其進行設置了。
??? 接下來就到了比較關鍵的部分了-ViewModel的構造函數。ReactiveUI關注定義和描述屬性和命令之間的相關關系,所以最重要的代碼就在ViewModel的構造函數中,可以將這部分工作看作是屬性之間的相互關聯。這種方法的好處是,所有交互的代碼都在這里,而不是分散在后臺代碼的事件處理和回調方法中。對于很多ViewModel來說,可能只有在構造函數中有一些代碼。
public AppViewModel() {GetDataFromTheInternet = new ReactiveAsyncCommand();var futureData = GetDataFromTheInternet.RegisterAsyncAction(I => {Thread.Sleep(5 * 1000);return String.Format("The Future will be {0}x as awesome!", i);});dataFromTheInternet = futureData.ToProperty(this, x => x.DataFromTheInternet); }??? 每一次用戶點擊按鈕的時候,Command的Execute方法就會被執行一次,每5分鐘就會向futureData這個Observable對象中傳入一個數據。程序運行結果如下:
?
?
???? 上面的代碼很簡潔,我們沒有任何顯示的定義異步方法比如聲明一個Task或者開一個線程,也沒有將返回的結果進行封送然后調用Dispatcher.BeginInvoke來更新UI界面的代碼。整個代碼看起來像是一個簡單的單線程的應用程序。而且進一步,這種方式極大的提高了可測試性。
??? 使用Dispatcher.BeginInvoke意味著我們假定Dispatcher存在并且起作用。但是在一個單元測試中,這個是不存在的。ReactiveUI會自動的刪除這些代碼并將他們換成默認的IScheduler而不使用Dispatcher.
???? 使用ReactiveAsyncCommad,代碼可以在后臺線程中運行,前臺UI依舊能夠響應用戶的操作。但是,一些長時間運行的操作,比如Web請求,并不需要頻繁的進行重復。這些數據應該緩存起來,使得不同的請求只請求一次。
?
5.ReactiveUI中的緩存
??? 緩存在實際開發中應用的很廣泛。最常用的做法是在本地維護一個查找表,以存儲最近獲取的數據,當再次請求這些數據時,先查看查找表中是否存在,如果存在就直接讀取,而不用再一次請求。每一種緩存方案都應該有緩存機制,例如規定緩存何時過期,如何移除過期的數據等等。有時候不恰當的機制,比如只往緩存中添加數據,而不移除過期的數據,會導致內存泄露。
??? 在ReactiveUI中,引入了一個稱之為MemorizingMRUCache的對象,如名字所示,他是一種以最近最常使用過的數據來作為緩存方案,它會移除一些在一定時間內沒有請求的數據,從而保證緩存集在一定的大小范圍內。
?
5.1使用MemorizingMRUCache
??? 調用MemorizingMRUCache的Get方法就可以從緩存中獲取對應的值,構造緩存時需要在其構造函數中出傳入緩存函數,該緩存函數必須是一種數學形態的,也就是說對于任何一個相同的給定參數,其返回值時也應該是相同的。另外一個需要注意的地方是他和QueuedAsyncMRUCache不同,他不是線程安全的。如果在多線程中使用該緩存對象,則需要加鎖。下面的例子簡單演示了MemorizingMRUCache的使用方法。
var cache = new MemoizingMRUCache<Int32, Int32>((x, ctx) => {Thread.Sleep(5 * 1000);return x * 100; },20);cache.Get(10);//第一次獲取,需要5秒 cache.Get(10);//第二次取值,立即返回 cache.Get(15);//也需要5秒?
5.2維護磁盤緩存
??? MemorizingMRUCache也可以將緩存數據從內存中存儲到磁盤上供以后使用,緩存的鍵可以是一個URL,值可以是該URL對應的臨時文件。當緩存文件不再需要時,調用OnRelease方法可以刪除這些臨時文件,下面是一些比較有用的函數。
- TryGet:視圖從緩存中獲取某一個鍵對應的值
- Invalidate:將某一個鍵對應的值的緩存進行清除,內部調用Release函數。
- InvalidateAll:清空所有緩存。
?
5.3 異步緩存結果
??? ObservableMemorizingMRUCache是一種線程安全的MemorizingMRUCache異步版本。如上所述,MemorizingMRUCache可以緩存一些需要大量計算的結果,但是它具有的缺點是其本身是單線程的結構,如果使用多線程訪問或者試圖緩存同時多個web請求的結果,就會產生問題。
??? ObservableMemorizingMRUCache解決了這一問題,同時提供了稱之為AsyncGet的方法,該方法返回一個IObservable對象。該對象在異步命令返回時返回,而且只執行一次。
??? 例如,假設我們要寫一個微博客戶端,需要獲取每條信息發布者的人物圖像,如果用傳統的foreach方法的話,可能會比較慢。即使采用傳統的異步方式獲取,仍然存在有獲取相同信息發布者的相同的人物圖像的情況。
??? ObservableMemorizingMRUCache解決了這個問題。在前面的例子中,我們獲取所有的微博信息集合,然后異步的請求發布者圖像信息。對以第一條記錄,我們發出WebRequest請求的時候,緩存中為空。然后我們請求第二條數據,這是時候,第一條數據可能還沒有返回,我們又請求了同一個圖像。如果某一個人發了50條微博信息,那么這樣的請求就會產生50次。
??? 當我們調用AsyncGet方法時,我們檢查緩存,而且也需要檢查請求列表。對于每一個可能的輸入,我們可以認為他有三種狀態,要么處于cache中,要么正在請求中,要么是全新的一個請求。ObservableAsyncMRUCache可以保證這三種狀態能夠以一種線程安全的方式正確處理。由于AsyncGet是一個異步方法,它能夠和ReactiveAsyncCommand很好的協同工作,我們可以將他作為RegisterAsyncObservable方法的一個參數。最后的結果是一個Command對象,該對象從后臺獲取數據,然后自動的維持最小的請求數據,減輕并發量,而且緩存了重復的請求數據。
??? 講了這么多,最后我們將以一個例子展示ReactiveUI的應用。
?
6.使用ReactiveUI開發一個異步圖片搜索工具
??? 這是一個使用Flickr來進行照片搜索的例子,當然您也可以使用Bing等搜索引擎。當用戶停止在輸入框輸入內容時,系統使用用戶輸入的關鍵字進行查詢,然后將查詢結果顯示出來。界面如下:
?
?
6.1 設計MVVM
??? 使用ReactiveUI框架的最主要目的是使用MVVM模式來開發程序,整個應用程序包含兩個類。MainWindow這個是View,對應的AppViewModel是ViewModel。
??? 在MainWindow中,我們需要創建一個AppViewModel簡單屬性,然后在MainWindows的構造函數的InitializeComponet()方法之前實例化AppViewModel對象。
public partial class MainWindow : Window {public AppViewModel ViewModel { get; protected set; }public MainWindow(){ViewModel = new AppViewModel();InitializeComponent();} }??? 對于AppViewModel類,使其繼承自ReactiveObject對象,然后定義一個SearchTerm屬性和ExecuteSearch命令,如下:
public class AppViewModel:ReactiveObject {String _SearchTerm;public String SearchTerm {get { return _SearchTerm; }set { this.RaiseAndSetIfChanged(x => x.SearchTerm, value); }}public ReactiveAsyncCommand ExecuteSearch { get; protected set; } }?
6.2將IObservable對象轉換為屬性
?
??? 在ReactiveUI中,我們可以將IObservable轉換為屬性,當Observable對象有新的值加入時,就會通知ReactiveObject對象更新其屬性值。
??? 前面講到,要實現這個轉換需要用到ObservableAsPropertyHelper類,這個類注冊一個Observable對象并存儲其最新值的一份拷貝。一般在ReactiveObject對象的RaisePropertyChanged方法調用時就會執行相應的操作。
ObservableAsPropertyHelper<List<FlickrPhoto>> _SearchResults; public List<FlickrPhoto> SearchResults {get { return this._SearchResults.Value; }} ObservableAsPropertyHelper<Visibility> _SpinnerVisibility; public Visibility SpinnerVisibility { get { return _SpinnerVisibility.Value; } }???? 上面創建一個屬性,用來控制Spinner控件的顯示,在應用程序忙時給出提示。然后,我們創建一個構造函數,定義兩個可選屬性,來方便測試。
public AppViewModel(ReactiveAsyncCommand testExecuteSearchCommand = null, IObservable<List<FlickrPhoto>> testSearchResults = null){ExecuteSearch = testExecuteSearchCommand ?? new ReactiveAsyncCommand();……}??? ViewModel中的屬性是彼此相互聯系的,傳統的方法很難簡潔的描述他們之間的關系,如“當程序正在搜索時,顯示Spinner”,這個簡單的關系通常會涉及到好幾個事件處理。使用ReactiveUI能夠以一種很整潔清晰的方式定義各個屬性之間的關系。
我們需要將屬性轉換為Observable對象,當搜索的關鍵字發生變化時,Observable就會返回一個對象。和之前的例子一樣,我們使用Throttle操作符來忽略一些不必要的頻繁的操作。我們并不想監聽鍵盤每一次按下事件,我們監聽變化的值,忽略兩次相同的查詢以及為空的查詢。
??? 最后,使用RxUI的InvoleCommand方法,該方法接受String類型,然后調用ExecuteSearch的Execute方法。
this.ObservableForProperty(x => x.SearchTerm).Throttle(TimeSpan.FromMilliseconds(800), RxApp.DeferredScheduler).Select(x => x.Value).DistinctUntilChanged().Where(x => !String.IsNullOrWhiteSpace(x)).InvokeCommand(ExecuteSearch);??? 當正在運行查詢時,我們需要顯示Spinner控件,ReactiveUI能夠描述這種狀態。ExecuteSearch有一個稱之為ItemsInFlight的IObservable<int>屬性,當有新的值產生或者移除時,會觸發該屬性發生變化,我們可以將這些信息和Visibility屬性結合起來,當該值等于0時隱藏,大于0時顯示。然后使用ToProperty操作符來創建ObservableAsPropertyHelper對象。
spinnerVisibility = ExecuteSearch.ItemsInflight.Select(x => x > 0 ? Visibility.Visible : Visibility.Collapsed).ToProperty(this, x => x.SpinnerVisibility, Visibility.Hidden);??? 然后,我們需要定義當命令觸發時應該執行的操作。在命令執行時,我們需要調用GetSearchResultsFromFlicker方法。值得注意的是,該方法的返回結果是一個Observable集合,每一次執行操作時,都會返回一個List類型的FlickerPhoto的Observable對象。
下面是構造函數中的方法和GetSearchResultsFromFlicker函數。
IObservable<List<FlickrPhoto>> results;if (testSearchResults != null){results = testSearchResults;}else{results = ExecuteSearch.RegisterAsyncFunction(term => GetSearchResultsFromFlickr((String)term));}_SearchResults = results.ToProperty(this, x => x.SearchResults, new List<FlickrPhoto>());private static List<FlickrPhoto> GetSearchResultsFromFlickr(string searchTerm) {var doc = XDocument.Load(String.Format(CultureInfo.InvariantCulture,"http://api.flickr.com/services/feeds/photos_public.gne?tags={0}&format=rss_200",HttpUtility.UrlEncode(searchTerm)));if (doc.Root == null)return null;var titles = doc.Root.Descendants("{http://search.yahoo.com/mrss/}title").Select(x => x.Value);var tagRegex = new Regex("<[^>]+>", RegexOptions.IgnoreCase);var descriptions =doc.Root.Descendants("{http://search.yahoo.com/mrss/}description").Select(x => tagRegex.Replace(HttpUtility.HtmlDecode(x.Value), ""));var items = titles.Zip(descriptions,(t, d) => new FlickrPhoto { Title = t, Description = d }).ToArray();var urls = doc.Root.Descendants("{http://search.yahoo.com/mrss/}thumbnail").Select(x => x.Attributes("url").First().Value);var ret = items.Zip(urls, (item, url) =>{item.Url = url; return item;}).ToList();return ret; }??? 程序的后臺代碼寫好了,前臺代碼如下,圖中紅色方框部分就是綁定ViewModel數據部分。可以看到UI界面上的控件都以聲明的方式基本都和ViewModel部分的數據綁定好了,使得View的后臺頁面基本上沒有什么代碼,您是否體會到了XAML的強大的數據綁定能力呢。
?
?
?? 編譯運行,下面是程序運行結果。
?
?
7.總結
??? 本文介紹了ReactiveUI這個和Rx結合緊密的MVVM框架,它使得我們開發的基于XAML的程序更加直觀,簡潔和可維護。另外使用Rx和ReactiveUI,使得程序的能夠很方便的進行測試,可以使用Rx和ReactiveUI來模擬整個流程,當然這也是所有MVVM框架要達到的目的。本文代碼點擊此處下載,希望本文對您了解ReactiveUI及MVVM有所幫助。
總結
以上是生活随笔為你收集整理的Reactive Extensions入门(5):ReactiveUI MVVM框架的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《降级论》《按时交作业的学生何以常穿脏袜
- 下一篇: Asp.Net_Mvc_IgnoreRo