Reactive Extensions入门(5):ReactiveUI MVVM框架
??? 從前面幾篇文章可以了解到,Rx作為LINQ的一種擴展,極大地簡化了異步編程。但Rx的用法不僅如此,由于其可高的擴展性,在其他很多方面也有所應(yīng)用。
??? 在前面例子中,我們使用代碼和UI界面上的元素打交道,這種方式在傳統(tǒng)的Winfom編程中很常見,但是在基于XAML構(gòu)造的界面這種應(yīng)用程序中,這樣顯得不是非常友好,XAML中聲明式編程可以使得程序更加簡潔,傳統(tǒng)的方式?jīng)]有利用到XAML中強大的綁定功能。之前,我們大量使用了諸如Observable.FromEvent這樣的操作,然后來使用后臺代碼來設(shè)置控件的屬性,這都是傳統(tǒng)的編程方式。
??? 當然,對于規(guī)模較小的程序來說,這種方式無可厚非。這種方式的最大的缺點在于,對于測試很不友好,要測試這樣的應(yīng)用程序很困難,我們需要創(chuàng)建UI控件并模擬輸入,這樣效率不高而且不可靠。另一個缺點是,這種方式使得代碼高度耦合而且脆弱。針對這些問題,一種稱為Model-View-ViewModel(MVVM)的設(shè)計模式逐漸發(fā)展起來。
??? 結(jié)合MVVM模式和Rx類庫,發(fā)展出了ReactiveUI這個MVVM框架。他能夠使得應(yīng)用程序可以管理,并能使用聲明式、函數(shù)式的方式來表達復(fù)雜的對象間的交互。換句話說,ReactiveUI能夠幫助我們描述屬性之間是如何聯(lián)系起來的,即使有些屬性與異步方法調(diào)用有關(guān)。
?
1. MVVM模式
??? Model-View-ViewModel模式是充分利用XAML設(shè)計平臺上的數(shù)據(jù)綁定功能而產(chǎn)生的一種設(shè)計模式。在該模式中,Model是用來表現(xiàn)應(yīng)用程序的數(shù)據(jù)以及與界面獨立的邏輯的核心對象。View就是UI控件及界面,比如窗體控件或者用戶自定義控件。值得注意的是對于同一數(shù)據(jù)對象,可能有多種表現(xiàn)形式,比如對于同一數(shù)據(jù)源,有的視圖用來顯示統(tǒng)計或者全局的信息,有的顯示每一項的詳細信息。
??? 一般我們很熟悉的是MVC模型,所以MVVM模式中的ViewModel是其特別的地方。從名字上看,ViewModel是一種針對視圖的模型。可能有點不好理解。舉個例子來說:在用戶注冊頁面(View)中,一般有輸入用戶名,密碼,重復(fù)輸入密碼這幾個輸入框。在這個視圖中,用戶名和密碼顯然是存在于Model中,但是 重復(fù)輸入密碼 這一項并不屬于Model,它顯然不應(yīng)該存在于真實的數(shù)據(jù)模型中,該項只是用在View中。
??? 在傳統(tǒng)的以XAML為界面的程序中,開發(fā)者一般使用頁面(View)的后臺的代碼來存儲這個重復(fù)輸入密碼值,但是這樣同樣存在可測試性和緊耦合的問題。例如,如果我們要測試“只有當密碼和重復(fù)密碼輸入的值匹配,提交按鈕才可以使用”這樣一個場景就變得有點困難。現(xiàn)在,我們將這個字段存在另一個稱之為ViewModel的對象中,這個ViewModel對象只是一個普通的類,他并不需要繼承自UI控件,我們可以將該對象看做是與View的交互邏輯。在我們的例子中,驗證兩次密碼是否匹配以及在匹配時讓提交按鈕可用,這些邏輯都應(yīng)該寫在ViewModel對象中。對于每一個View,都應(yīng)該有一個對應(yīng)的ViewModel對象。
?
1.1 ViewModeld的理念
??? MVVM最強大的一方面在于它的目標是將一個命令(command)或者屬性(property)是什么和如何執(zhí)行分開來。ViewModel是對屬性和命令的一種思考。在傳統(tǒng)的基于Codebehind的用戶交互框架中,開發(fā)者需要思考控件的事件和屬性。當以這種方式編寫代碼時,意味著事件和相應(yīng)的控件緊緊的聯(lián)系在一起。使得測試變得困難,因為我們需要模擬出控件的動作才能測試控件對應(yīng)的事件及功能是否正常。
當使用MVVM的ViewModels時,最重要的是將這兩部分邏輯分開來。在View中決定了這些控件如何被觸發(fā),同時,控件對應(yīng)的一些屬性利用XAML的綁定技術(shù)和ViewModel綁定起來。
?
1.2 MVVM框架的作用
??? 現(xiàn)在有很多開源的MVVM框架可以使用了,如MVVMLight、Prism,這些框架框架各有優(yōu)點。但是他們都提供了實現(xiàn)MVVM模式的最基本要素。首先,這些框架為ViewModel對象提供了一個基類,當這些對象的屬性在屬性值發(fā)生改變時會得到通知,這個是通過實現(xiàn)INotifyPropertyChanged接口來完成的,這個接口很關(guān)鍵,因為他通知View需要更新綁定到界面上的數(shù)據(jù)。MVVM提供了處理命令的一套系統(tǒng),當用戶發(fā)出一些命令時它能夠很好的處理。這是通過實現(xiàn)ICommand接口來實現(xiàn)的,這個接口通常包含在UI控件中。
?
2.ReactiveUI庫
??? ReactiveUI類庫是實現(xiàn)了MVVM模式的框架,他移除了一些Rx和用戶界面進行交互的代碼。ReactiveUI的核心思想是使開發(fā)者能夠?qū)傩宰兏约笆录D(zhuǎn)換為IObservable對象,然后在需要的時候使用IObservable對象將這些對象轉(zhuǎn)換到屬性中來。他的另一個核心目標是可以在ViewModel中相關(guān)屬性發(fā)生變化時可以可執(zhí)行相應(yīng)的命令。雖然其他的框架也允許這么做,但是ReactiveUI會在依賴屬性變更時自動的去更新結(jié)果,而不需要通過拉或者調(diào)用類似UpdateTheUI之類的方法。
?
2.1 核心類
ReactiveObject:它是ViewModel對象,該對象實現(xiàn)了INotifyPropertyChanged接口。除此之外,該對象也提供了一個稱之為Changed的IObservable接口,允許其他對象來注冊,從而使得該對象屬性變更時能夠得到通知。使用Rx中強大的操作符,我們還可以追蹤到一些狀態(tài)是如何改變的。
ReactiveValidateObject:該對象繼承自ReactiveObject對象,它通過實現(xiàn)IDataErrorInfo接口,利用DataAnnotations來驗證對象。因此屬性的值可以使用一些限制標記,UI界面能夠自動的在屬性的值違反這些限制時顯示出這些錯誤。
ObservableAsPropertyHelper<T>:該類可以很容易的將IObservable對想轉(zhuǎn)換為一個屬性,該屬性存儲該對象的最新值,并且在屬性值發(fā)生改變時能夠觸發(fā)NofityPropertyChanged事件。使用該類,我們能夠從IObservable中派生出一些新的屬性。
ReactiveCommand:該類實現(xiàn)了ICommand和IObservable接口,并且當Execute執(zhí)行時OnNext方法就會被執(zhí)行。該對象的CanExecute可以通過IObservable<bool>來定義。
ReactiveAsyncCommand:該對象繼承自ReactiveCommand,并且封裝了一種通用的模式。即“觸發(fā)一步命令,然后將結(jié)果封送到dispather線程中”該對象也允許設(shè)置最大并行值。當達到最大值時,CanExecute方法返回false。
?
3.使用ReactiveObject實現(xiàn)ViewModels
和其他MVVM框架一樣,ReactiveUI框架有一個對象來作為ViewModel類。該對象和基于傳統(tǒng)的實現(xiàn)了ViewModel對象的MVVM框架如Foundation,Cliburn.Micro類似。但是最大的不同在于,ReactiveUI能夠很容易的通過名為Changed的IObservable接口注冊事件變化。在任何一個屬性發(fā)生變化時,都會觸發(fā)通知,客戶端通常只需要關(guān)注感興趣的一兩個變化了的屬性。使用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語法看起來過有點奇怪。方法中第一個參數(shù)是通過匿名方法定義的一系列屬性。在上面的例子中,我們關(guān)心的是神馬時候Password或者PasswordConfirm發(fā)生變化。最后一個參數(shù)和Zip操作符中的類似,他使用一個匿名方法來將兩個結(jié)果結(jié)合起來,然后返回結(jié)果。當這兩個屬性中的任何一個發(fā)生變化時,方法就會執(zhí)行,并以IObservable的形式返回執(zhí)行結(jié)果,在上面的例子中就是passwordIsValid這個對象。
??? 對于ReactiveObject,值得注意的是,屬性必須明確的使用特定的語法進行定義。因為簡單的get,set并沒有實現(xiàn)INotifyPropertyChanged,從而不會通知ReactiveObject對象該屬性發(fā)生了改變。唯一例外的就是,如果一個屬性在構(gòu)造器中初始化了,在以后的程序中不會發(fā)生改變。在ReactiveObject中,屬性的命名也需要注意,用作屬性的私有字段必須為屬性名稱前面加上下劃線。下面的例子展示了如何使用ReactiveObject聲明一個可讀寫的屬性。
public class AppViewModel : ReactiveObject {int _SomeProp;public int SomeProp{get { return _SomeProp; }set { this.RaiseAndSetIfChanged(x => x.SomeProp, value); }} }傳統(tǒng)的實現(xiàn)IpropertyChangeNofity接口的實現(xiàn)方法如下:
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實現(xiàn)了ReactiveUI的核心功能之一,它使得開發(fā)者能夠很容易將相關(guān)屬性變化用IObservable表示。該功能使得可以直接使用Rx以聲明的方式創(chuàng)建狀態(tài)機。
??? 除了使用Rx來描述復(fù)雜的異步操作事件之外,Rx和ReactiveUI結(jié)合可以使得對象在某個特定的狀態(tài)下可以得到通知,即使這種狀態(tài)涉及到多個不同的對象或者屬性。
?
4. ReactiveCommand
ReactiveCommand實現(xiàn)了ICommand接口,他可以模擬簡單的ICommand實現(xiàn)。我們可以將它看做是一種ICommand,可以使用Create靜態(tài)方法創(chuàng)建。
var cmd = ReactiveCommand.Create(x => true, x => Console.WriteLine(x)); cmd.CanExecute(null); //方法輸出true cmd.CanExecute("Hello"); //方法輸出"Hello"下面構(gòu)造了一個Command,該Command只在鼠標松開時觸發(fā)。
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構(gòu)造Command。通常我們使用WhenAny創(chuàng)建IObservable然后構(gòu)造Command對象。大多數(shù)情況下,只有當特定的屬性被設(shè)置或者取消設(shè)置時會觸發(fā)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,當在兩次密碼不匹配時,按鈕或者菜單就會呈現(xiàn)出灰色。當密碼或者重復(fù)密碼輸入框中的值發(fā)生變化時,ReactiveCommand就會重新求值,來決定是否使得按鈕或者菜單可用。
??? 值得注意的是,當屬性發(fā)生變化時,Command的CanExecute會立即自動更新,而不依賴于CommandManager.RequerySuggested。在WPF或者Silverlight中存在這個bug,除非你切換焦點或者點擊,按鈕不會重新改變其狀態(tài)。使用IObservable意味著Commanding框架確切的知道在狀態(tài)發(fā)生改變時,不需要重新手動執(zhí)行頁面上的每一個Command對象。
??? ReactiveCommand對象本身可以被注冊,并且在執(zhí)行Exectue方法時,提供一些有用的信息。這表明,訂閱者可以執(zhí)行一些Reactive可以執(zhí)行的一些動作,使得我們能夠更好的進行控制。如下:
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轉(zhuǎn)化為Properties
??? 使用WhenAny方法,可以監(jiān)視對象屬性的變化,并針對這些變化生成IObservable對象。但是有時候,我們想將這些生成的IObservable對象設(shè)置為一種輸出屬性。想象一下有這樣一個場景,有一個取色器,用戶能夠通過3個Slide分別設(shè)置R,G,B值。每一個Slide可以使用ViewModel對象來表示,取值范圍為0到1。為了顯示結(jié)果,我們需要將RGB合成為一個XAML顏色對象。當RGB中的任何一個發(fā)生變化時,我們需要更新顏色屬性。
??? 我們可以常簡單的通過WhenAny創(chuàng)建一個IObservable<Color>對象,但是我們想將這個值存回到屬性中。ReactiveUI提供了一個稱之為ObservableAsPropertyHelper的對象,該對象可以存儲IObservable中的最新值。為了演示這一操作,我們需要創(chuàng)建一個“輸出屬性”
ObservableAsPropertyHelper<Color> finalColor; public Color FinalColor {get {return finalColor.Value;} }??? 注意到屬性并沒有set方法,這是因為屬性是由IObservable生成的,而不需要手動設(shè)定。在ViewModel的構(gòu)造函數(shù)中,我們將描述如何從RGB產(chǎn)生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);??? 這一步只需在構(gòu)造函數(shù)中執(zhí)行一次。現(xiàn)在只要Red,Green,或者Blue中的任何一個發(fā)生變化,FinalColor對象都會更新以反映最新的變化值。
??? ReactiveObject和ReactiveCommad是創(chuàng)建ViewModel對象的兩個核心工具。使用它們我們可以使用屬性和命令以及通過描述屬性和命令之間的動態(tài)關(guān)系來構(gòu)建一個View。當我們關(guān)心狀態(tài)變化,以及某一個屬性的變化對另外一個變化產(chǎn)生的影像時時,我們可以將屬性轉(zhuǎn)換為IObservable對象。這一點可以幫助我們很好地測試ViewModel對象。
ReactiveUI還有一些功能能夠幫助我們在用戶界面上優(yōu)雅的處理異步方法調(diào)用。幾乎大部分的應(yīng)用程序都需要運行后臺程序,Reactive的靈活方便的異步操作能力使得ReactiveUI在獲取這些異步計算結(jié)果時變得很容易。
?
4.2使用ReactiveAsyncCommand處理異步方法調(diào)用
??? 在Winform或者WPF應(yīng)用程序中,如果事件執(zhí)行需要耗費很長時間,比如讀取一個很大的文件,那么UI很容易卡死。這是因為程序在忙于處理文件讀寫操作或者在等待網(wǎng)絡(luò)數(shù)據(jù)傳輸而不能夠刷新用戶界面。在Silverligh或者Windows Phone中通過規(guī)定UI線程不允許阻塞來解決了這一問題。
??? 通常解決這一問題的辦法是另外開一個線程或者使用線程池來處理這些耗時操作,但是這又帶來了第二個問題,那就是所有基于XAML的框架都是線程關(guān)聯(lián)的(thread affinity),這意味著,我們只能夠從創(chuàng)建該對象的那個線程訪問該對象。所以如果您在非UI線程中更新UI,比如執(zhí)行完了一些操作后直接進行類似textbox.text=results這類的更新就會拋出錯誤。因為非UI線程不能夠更新UI上的對象。
傳統(tǒng)的解決這一方法是在更新UI操作時調(diào)用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狀態(tài)。稍微友好一些的用戶界面在后臺正在進行的操作時給UI界面一些提示,比如在界面上顯示,“程序正在進行xxx……”的提示,這樣顯得更加友好。
??? 由于ReactiveAsyncCommand直接繼承自ReactiveCommand,所以它能做基類的所有功能。使用Execute,使得Command開始在后臺執(zhí)行時并可以通知用戶。ReactiveAsyncCommand和ReactiveCommand不同之處在于,它內(nèi)建了能夠自動跟蹤后臺線程中運行的任務(wù)的數(shù)量。
??? 下面是一個簡單的使用Command的例子,它在后臺線程的Task中運行,并且只運行一次。
var cmd = new ReactiveAsyncCommand(); cmd.RegisterAsyncAction(i => {Thread.Sleep((int)i * 1000); });cmd.Execute(5); cmd.CanExecute(5);//False??? ReactiveAsyncCommand對象中是使用RegisterAsyncAction來注冊異步執(zhí)行操作的。它能夠注冊異步方法和同步方法,這些方法將會在后臺線程中執(zhí)行,并返回IObservable數(shù)據(jù)表示執(zhí)行結(jié)果會在未來的某一時刻到來。IObservale通常對應(yīng)Command調(diào)用。每一次執(zhí)行Execute方法將會將結(jié)果存入到IObservable對象中。
?
4.3構(gòu)造一個ViewModel例子
??? 講了這麼多ReactiveUI框架的幾個重要對象。現(xiàn)在用一個簡單的View以及與之相關(guān)聯(lián)的VeiwModel來展示如何使用ViewModel。本例子將展示如何執(zhí)行一些簡單的和按鈕相關(guān)的命令,并模擬在后臺執(zhí)行一些費時的操作。然后將結(jié)果顯示在UI界面上。
??? 首先來看看我們的前臺頁面,也就是View,在這里我建立的是一個簡單的WPF應(yīng)用程序。
<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參數(shù)綁定到我們的ViewModel對象上。這樣,當我們使用XAML數(shù)據(jù)綁定時,這些元素相對于ViewModel而不是View來進行綁定。然后我們定義了一個TextBlock,將其內(nèi)容綁定到DataFromTheInternet屬性上。最后,我們綁定Button的Command屬性到我們再ViewModel中定義的一個稱之為GetDataFromTheInternet的Command對象上。相關(guān)的定義以及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方法創(chuàng)建了一個名為ViewModel的普通屬性,然后我們再構(gòu)造函數(shù)的InitializeComponet方法之前初始化了該屬性。接著,我們定義了一個ViewModel類來對我們的View進行建模。通過ObservableAsPropertyHelper以及ReactiveAsyncCommand定義了一個輸出屬性。必須是屬性才在XAML中綁定,屬性的setter是Protected的,因為我們只需要在構(gòu)造函數(shù)中進行實例化,之后就不會再對其進行設(shè)置了。
??? 接下來就到了比較關(guān)鍵的部分了-ViewModel的構(gòu)造函數(shù)。ReactiveUI關(guān)注定義和描述屬性和命令之間的相關(guān)關(guān)系,所以最重要的代碼就在ViewModel的構(gòu)造函數(shù)中,可以將這部分工作看作是屬性之間的相互關(guān)聯(lián)。這種方法的好處是,所有交互的代碼都在這里,而不是分散在后臺代碼的事件處理和回調(diào)方法中。對于很多ViewModel來說,可能只有在構(gòu)造函數(shù)中有一些代碼。
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方法就會被執(zhí)行一次,每5分鐘就會向futureData這個Observable對象中傳入一個數(shù)據(jù)。程序運行結(jié)果如下:
?
?
???? 上面的代碼很簡潔,我們沒有任何顯示的定義異步方法比如聲明一個Task或者開一個線程,也沒有將返回的結(jié)果進行封送然后調(diào)用Dispatcher.BeginInvoke來更新UI界面的代碼。整個代碼看起來像是一個簡單的單線程的應(yīng)用程序。而且進一步,這種方式極大的提高了可測試性。
??? 使用Dispatcher.BeginInvoke意味著我們假定Dispatcher存在并且起作用。但是在一個單元測試中,這個是不存在的。ReactiveUI會自動的刪除這些代碼并將他們換成默認的IScheduler而不使用Dispatcher.
???? 使用ReactiveAsyncCommad,代碼可以在后臺線程中運行,前臺UI依舊能夠響應(yīng)用戶的操作。但是,一些長時間運行的操作,比如Web請求,并不需要頻繁的進行重復(fù)。這些數(shù)據(jù)應(yīng)該緩存起來,使得不同的請求只請求一次。
?
5.ReactiveUI中的緩存
??? 緩存在實際開發(fā)中應(yīng)用的很廣泛。最常用的做法是在本地維護一個查找表,以存儲最近獲取的數(shù)據(jù),當再次請求這些數(shù)據(jù)時,先查看查找表中是否存在,如果存在就直接讀取,而不用再一次請求。每一種緩存方案都應(yīng)該有緩存機制,例如規(guī)定緩存何時過期,如何移除過期的數(shù)據(jù)等等。有時候不恰當?shù)臋C制,比如只往緩存中添加數(shù)據(jù),而不移除過期的數(shù)據(jù),會導(dǎo)致內(nèi)存泄露。
??? 在ReactiveUI中,引入了一個稱之為MemorizingMRUCache的對象,如名字所示,他是一種以最近最常使用過的數(shù)據(jù)來作為緩存方案,它會移除一些在一定時間內(nèi)沒有請求的數(shù)據(jù),從而保證緩存集在一定的大小范圍內(nèi)。
?
5.1使用MemorizingMRUCache
??? 調(diào)用MemorizingMRUCache的Get方法就可以從緩存中獲取對應(yīng)的值,構(gòu)造緩存時需要在其構(gòu)造函數(shù)中出傳入緩存函數(shù),該緩存函數(shù)必須是一種數(shù)學(xué)形態(tài)的,也就是說對于任何一個相同的給定參數(shù),其返回值時也應(yīng)該是相同的。另外一個需要注意的地方是他和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也可以將緩存數(shù)據(jù)從內(nèi)存中存儲到磁盤上供以后使用,緩存的鍵可以是一個URL,值可以是該URL對應(yīng)的臨時文件。當緩存文件不再需要時,調(diào)用OnRelease方法可以刪除這些臨時文件,下面是一些比較有用的函數(shù)。
- TryGet:視圖從緩存中獲取某一個鍵對應(yīng)的值
- Invalidate:將某一個鍵對應(yīng)的值的緩存進行清除,內(nèi)部調(diào)用Release函數(shù)。
- InvalidateAll:清空所有緩存。
?
5.3 異步緩存結(jié)果
??? ObservableMemorizingMRUCache是一種線程安全的MemorizingMRUCache異步版本。如上所述,MemorizingMRUCache可以緩存一些需要大量計算的結(jié)果,但是它具有的缺點是其本身是單線程的結(jié)構(gòu),如果使用多線程訪問或者試圖緩存同時多個web請求的結(jié)果,就會產(chǎn)生問題。
??? ObservableMemorizingMRUCache解決了這一問題,同時提供了稱之為AsyncGet的方法,該方法返回一個IObservable對象。該對象在異步命令返回時返回,而且只執(zhí)行一次。
??? 例如,假設(shè)我們要寫一個微博客戶端,需要獲取每條信息發(fā)布者的人物圖像,如果用傳統(tǒng)的foreach方法的話,可能會比較慢。即使采用傳統(tǒng)的異步方式獲取,仍然存在有獲取相同信息發(fā)布者的相同的人物圖像的情況。
??? ObservableMemorizingMRUCache解決了這個問題。在前面的例子中,我們獲取所有的微博信息集合,然后異步的請求發(fā)布者圖像信息。對以第一條記錄,我們發(fā)出WebRequest請求的時候,緩存中為空。然后我們請求第二條數(shù)據(jù),這是時候,第一條數(shù)據(jù)可能還沒有返回,我們又請求了同一個圖像。如果某一個人發(fā)了50條微博信息,那么這樣的請求就會產(chǎn)生50次。
??? 當我們調(diào)用AsyncGet方法時,我們檢查緩存,而且也需要檢查請求列表。對于每一個可能的輸入,我們可以認為他有三種狀態(tài),要么處于cache中,要么正在請求中,要么是全新的一個請求。ObservableAsyncMRUCache可以保證這三種狀態(tài)能夠以一種線程安全的方式正確處理。由于AsyncGet是一個異步方法,它能夠和ReactiveAsyncCommand很好的協(xié)同工作,我們可以將他作為RegisterAsyncObservable方法的一個參數(shù)。最后的結(jié)果是一個Command對象,該對象從后臺獲取數(shù)據(jù),然后自動的維持最小的請求數(shù)據(jù),減輕并發(fā)量,而且緩存了重復(fù)的請求數(shù)據(jù)。
??? 講了這么多,最后我們將以一個例子展示ReactiveUI的應(yīng)用。
?
6.使用ReactiveUI開發(fā)一個異步圖片搜索工具
??? 這是一個使用Flickr來進行照片搜索的例子,當然您也可以使用Bing等搜索引擎。當用戶停止在輸入框輸入內(nèi)容時,系統(tǒng)使用用戶輸入的關(guān)鍵字進行查詢,然后將查詢結(jié)果顯示出來。界面如下:
?
?
6.1 設(shè)計MVVM
??? 使用ReactiveUI框架的最主要目的是使用MVVM模式來開發(fā)程序,整個應(yīng)用程序包含兩個類。MainWindow這個是View,對應(yīng)的AppViewModel是ViewModel。
??? 在MainWindow中,我們需要創(chuàng)建一個AppViewModel簡單屬性,然后在MainWindows的構(gòu)造函數(shù)的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對象轉(zhuǎn)換為屬性
?
??? 在ReactiveUI中,我們可以將IObservable轉(zhuǎn)換為屬性,當Observable對象有新的值加入時,就會通知ReactiveObject對象更新其屬性值。
??? 前面講到,要實現(xiàn)這個轉(zhuǎn)換需要用到ObservableAsPropertyHelper類,這個類注冊一個Observable對象并存儲其最新值的一份拷貝。一般在ReactiveObject對象的RaisePropertyChanged方法調(diào)用時就會執(zhí)行相應(yīng)的操作。
ObservableAsPropertyHelper<List<FlickrPhoto>> _SearchResults; public List<FlickrPhoto> SearchResults {get { return this._SearchResults.Value; }} ObservableAsPropertyHelper<Visibility> _SpinnerVisibility; public Visibility SpinnerVisibility { get { return _SpinnerVisibility.Value; } }???? 上面創(chuàng)建一個屬性,用來控制Spinner控件的顯示,在應(yīng)用程序忙時給出提示。然后,我們創(chuàng)建一個構(gòu)造函數(shù),定義兩個可選屬性,來方便測試。
public AppViewModel(ReactiveAsyncCommand testExecuteSearchCommand = null, IObservable<List<FlickrPhoto>> testSearchResults = null){ExecuteSearch = testExecuteSearchCommand ?? new ReactiveAsyncCommand();……}??? ViewModel中的屬性是彼此相互聯(lián)系的,傳統(tǒng)的方法很難簡潔的描述他們之間的關(guān)系,如“當程序正在搜索時,顯示Spinner”,這個簡單的關(guān)系通常會涉及到好幾個事件處理。使用ReactiveUI能夠以一種很整潔清晰的方式定義各個屬性之間的關(guān)系。
我們需要將屬性轉(zhuǎn)換為Observable對象,當搜索的關(guān)鍵字發(fā)生變化時,Observable就會返回一個對象。和之前的例子一樣,我們使用Throttle操作符來忽略一些不必要的頻繁的操作。我們并不想監(jiān)聽鍵盤每一次按下事件,我們監(jiān)聽變化的值,忽略兩次相同的查詢以及為空的查詢。
??? 最后,使用RxUI的InvoleCommand方法,該方法接受String類型,然后調(diào)用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能夠描述這種狀態(tài)。ExecuteSearch有一個稱之為ItemsInFlight的IObservable<int>屬性,當有新的值產(chǎn)生或者移除時,會觸發(fā)該屬性發(fā)生變化,我們可以將這些信息和Visibility屬性結(jié)合起來,當該值等于0時隱藏,大于0時顯示。然后使用ToProperty操作符來創(chuàng)建ObservableAsPropertyHelper對象。
spinnerVisibility = ExecuteSearch.ItemsInflight.Select(x => x > 0 ? Visibility.Visible : Visibility.Collapsed).ToProperty(this, x => x.SpinnerVisibility, Visibility.Hidden);??? 然后,我們需要定義當命令觸發(fā)時應(yīng)該執(zhí)行的操作。在命令執(zhí)行時,我們需要調(diào)用GetSearchResultsFromFlicker方法。值得注意的是,該方法的返回結(jié)果是一個Observable集合,每一次執(zhí)行操作時,都會返回一個List類型的FlickerPhoto的Observable對象。
下面是構(gòu)造函數(shù)中的方法和GetSearchResultsFromFlicker函數(shù)。
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數(shù)據(jù)部分。可以看到UI界面上的控件都以聲明的方式基本都和ViewModel部分的數(shù)據(jù)綁定好了,使得View的后臺頁面基本上沒有什么代碼,您是否體會到了XAML的強大的數(shù)據(jù)綁定能力呢。
?
?
?? 編譯運行,下面是程序運行結(jié)果。
?
?
7.總結(jié)
??? 本文介紹了ReactiveUI這個和Rx結(jié)合緊密的MVVM框架,它使得我們開發(fā)的基于XAML的程序更加直觀,簡潔和可維護。另外使用Rx和ReactiveUI,使得程序的能夠很方便的進行測試,可以使用Rx和ReactiveUI來模擬整個流程,當然這也是所有MVVM框架要達到的目的。本文代碼點擊此處下載,希望本文對您了解ReactiveUI及MVVM有所幫助。
總結(jié)
以上是生活随笔為你收集整理的Reactive Extensions入门(5):ReactiveUI MVVM框架的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《降级论》《按时交作业的学生何以常穿脏袜
- 下一篇: VHDL+Verilog良好的代码编写风