2010-10-19 23 views
0

我試圖從Silverlight應用程序中刪除更多的傳統事件處理程序,以支持使用多個Rx查詢來提供更好,更易於管理,行爲抽象。結合反應框架(Rx)查詢提供正確的UI行爲的問題

我需要解決的問題,但不能以我想要的方式完全破解它,正在使搜索屏幕的行爲發揮作用。這是非常標準的東西。這是它應該如何表現:

  • 我有一個文本框中,用戶可以進入搜索文本
  • 如果沒有文本(或空格只)那麼搜索按鈕禁用
  • 當有非空白文本那麼搜索按鈕啓用
  • 當用戶點擊搜索文本框中搜索按鈕禁用
  • 結果回來文本框中搜索按鈕啓用

我有這些觀測值(通過一些擴展方法在標準事件創建)一起工作:

IObservable<IEvent<TextChangedEventArgs>> textBox.TextChangedObservable() 
IObservable<IEvent<RoutedEventArgs>> button.ClickObservable() 
IObservable<IEvent<LoadingDataEventArgs>> dataSource.LoadingDataObservable() 
IObservable<IEvent<LoadedDataEventArgs>> dataSource.LoadedDataObservable() 

我的這些疑問,在目前的工作:

IObservable<bool> dataSourceIsBusy = 
    dataSource.LoadingDataObservable().Select(x => true) 
    .Merge(dataSource.LoadedDataObservable().Select(x => false)); 

IObservable<string> textBoxText = 
    from x in textBox.TextChangedObservable() 
    select textBox.Text.Trim(); 

IObservable<bool> textBoxTextIsValid = 
    from text in textBoxText 
    let isValid = !String.IsNullOrEmpty(text) 
    select isValid; 

IObservable<string> searchTextReady = 
    from x in button.ClickObservable() 
    select textBox.Text.Trim(); 

而且那麼這些訂閱將它連接起來:

buttonIsEnabled.Subscribe(x => button.IsEnabled = x); 
dataSourceIsBusy.Subscribe(x => textBox.IsEnabled = !x); 
searchTextReady.Subscribe(x => this.ExecuteSearch(x)); 

(我確實保留對Subscribe方法返回的一次性用品的參考。我已將.ObserveOnDispatcher()添加到每個observable,以確保代碼在UI線程上運行。)

現在,雖然這有效,但有一件事情會讓我感到困擾。 searchTextReady中的select語句調用textBox.Text.Trim()來獲取當前修剪的搜索文本,但我已經在textBoxText可觀察值中完成了此操作。我真的不想重複自己,所以我想創建一個查詢,結合這些observables,這是我失敗的地方。

當我嘗試下面的查詢我得到重入調用來執行搜索:

IObservable<string> searchTextReady = 
    from text in textBoxText 
    from x in button.ClickObservable() 
    select text; 

下面的查詢,似乎爲第一查詢工作,但後來我每次更改文本中

IObservable<string> searchTextReady = 
    from text in button.ClickObservable() 
     .CombineLatest(textBoxText, (c, t) => t) 
    select text; 

下面的查詢需要進一步的文本改變點擊搜索按鈕後,會出現,然後未能再次運行:

文本框後的搜索無需點擊搜索按鈕被自動執行
IObservable<string> searchTextReady = 
    from text in textBoxText 
     .SkipUntil(button.ClickObservable()) 
     .TakeUntil(dataSource.LoadingDataObservable()) 
    select text; 

任何想法,我可以使這項工作?

回答

1

感謝大家在這個問題上的幫助。我無法給任何人答案,因爲我發現解決方案是使用.Switch()運營商,沒有人給出答案。

由於我確實找到了我的問題的答案,我以爲我會在這裏發佈它來幫助其他人嘗試做我所做的。

我原來的問題的一部分什麼,我不想重複代碼。所以我介紹了Func<string, bool> textIsValid,可以在我的IObservable<bool> textBoxTextIsValid和我想要的IObservable<string> searchTextReady中重複使用。

IObservable<string> searchTextReady = 
    (from text in textBoxText 
    select (from x in button.ClickObservable().TakeUntil(textBoxText) 
      where textIsValid(text) 
      select text) 
    ).Switch(); 

所以.Switch()作品通過採取IObservable<IObservable<string>>和壓扁它爲IObservable<string>

Func<string, bool> textIsValid = t => !String.IsNullOrEmpty(t); 

現在searchTextReady可以如下定義。

很容易創建一個IObservable<IObservable<string>> - 只需針對observable編寫查詢並選擇其他可觀察對象。

例如:

IObservable<Unit> xs= ...; 
IObservable<string> ys= ...; 

IObservable<IObservable<string>> zss = xs.Select(x => ys); 

IObservable<string> zs = zss.Switch(); 

但這並不做任何特別有用。

如果它周圍改變:

IObservable<Unit> clicks = ...; 
IObservable<string> texts = ...; 
Func<string, bool> isValid = ...; 

IObservable<IObservable<string>> zss = 
    texts.Select(t => 
     clicks 
      .Take(1) 
      .TakeUntil(texts) 
      .Where(x => isValid(t)) 
      .Select(x => t)); 

IObservable<string> zs = zss.Switch(); 

最後觀察到的,可以描述爲:

「每當文本更改只拿 接下來點擊提供的文本 不再次更改並使文本 有效,然後選擇文本更改爲的值 。「

我希望這個答案可以幫助別人。

0

你試過ForkJoin

喜歡的東西:

IObservable<string> searchTextReady = Observable.ForkJoin(textBoxText, button.ClickObservable()); 
    searchTextReady.Subscribe(.... 
+0

對不起,但'ForkJoin'只結合了observables的最後一個值(它們也需要是相同的類型)。每次點擊搜索按鈕時,我都想讓'searchTextReady'觀察值觸發並返回最新的,而不是最後的'textBoxText'值。不管怎麼說,還是要謝謝你。 – Enigmativity 2010-10-19 02:25:41

5

這幾樣東西都是自己非常棘手,所以我最後寫an M-V-VM + Rx library幫助我 - 事實證明,與此庫,這個任務很簡單;這裏是整個代碼,my blog介紹了更多關於這些類是如何工作的:

public class TextSearchViewModel 
{ 
    public TextSearchViewModel 
    { 
     // If there is no text (or whitespace only) then the search button is disabled. 
     var isSearchEnabled = this.ObservableForProperty(x => x.SearchText) 
      .Select(x => !String.IsNullOrWhitespace(x.Value)); 

     // Create an ICommand that represents the Search button 
     // Setting 1 at a time will make sure the Search button disables while search is running 
     DoSearch = new ReactiveAsyncCommand(isSearchEnabled, 1/*at a time*/); 

     // When the user clicks search the text box and the search button are both disabled. 
     var textBoxEnabled = DoSearch.ItemsInflight 
      .Select(x => x == 0); 

     // Always update the "TextboxEnabled" property with the latest textBoxEnabled IObservable 
     _TextboxEnabled = this.ObservableToProperty(textBoxEnabled, 
      x => x.TextboxEnabled, true); 

     // Register our search function to run in a background thread - for each click of the Search 
     // button, the searchResults IObservable will produce a new OnNext item 
     IObservable<IEnumerable<MyResult>> searchResults = DoSearch.RegisterAsyncFunction(textboxText => { 
      var client = new MySearchClient(); 
      return client.DoSearch((string)textboxText); 
     }); 

     // Always update the SearchResults property with the latest item from the searchResults observable 
     _SearchResults = this.ObservableToProperty(searchResults, x => x.SearchResults); 
    } 

    // Create a standard INotifyPropertyChanged property 
    string _SearchText; 
    public string SearchText { 
     get { return _SearchText; } 
     set { this.RaiseAndSetIfChanged(x => x.SearchText, value); } 
    } 

    // This is a property who will be updated with the results of an observable 
    ObservableAsPropertyHelper<bool> _TextboxEnabled; 
    public bool TextboxEnabled { 
     get { return _TextboxEnabled.Value; } 
    } 

    // This is an ICommand built to do tasks in the background 
    public ReactiveAsyncCommand DoSearch { get; protected set; } 

    // This is a property who will be updated with the results of an observable 
    ObservableAsPropertyHelper<IEnumerable<MyResult>> _SearchResults; 
    public IEnumerable<MyResult> SearchResults { 
     get { return _SearchResults.Value; } 
    } 
} 
+0

我喜歡你在這裏所做的,所以我給了你一個投票權,但它並不直接回答我的問題,所以我不準備接受它作爲答案。如果沒有其他事情出現,我會看看如何調整你爲我的需求所做的。現在儘管這會代表一個相當大的代碼更改。雖然工作很好! – Enigmativity 2010-10-19 23:52:15

+0

隨意從代碼庫中偷取零碎 - 重要的想法雖然是你不應該在事件/視圖級別使用Rx,但它在ViewModel級別使用它會好得多(並且可測試!財產變更通知和命令調用的決定。 – 2010-10-20 02:09:41

+0

另外,在設置可觀察對象的初始狀態時,是否會添加一個StartWith幫助您? – 2010-10-20 02:12:26

0

DistinctUntilChanged是你在找什麼。

E.g.像這樣的東西應該工作:

// low level observables 
var dataSourceLoading = ... // "loading" observable 
var dataSourceLoaded = ... // "loaded" observable 
var textChange = Observable.FromEvent<TextChangedEventArgs>(MyTextBox, "TextChanged"); 
var click = Observable.FromEvent<RoutedEventArgs>(MyButton, "Click"); 

// higher level observables 
var text = textChange.Select(_ => MyTextBox.Text.Trim()); 
var emptyText = text.Select(String.IsNullOrWhiteSpace); 
var searchInProgress = dataSourceLoading.Select(_ => true).Merge(dataSourceLoaded.Select(_ => false)); 

// enable/disable controls 
searchInProgress.Merge(emptyText) 
    .ObserveOnDispatcher() 
    .Subscribe(v => MyButton.IsEnabled = !v); 
searchInProgress 
    .ObserveOnDispatcher() 
    .Subscribe(v => MyTextBox.IsEnabled = !v); 

// load data 
click 
    .CombineLatest(text, (c,t) => new {c,t}) 
    .DistinctUntilChanged(ct => ct.c) 
    .Subscribe(ct => LoadData(ct.t)); 
+0

感謝您的回答,但它不起作用。 'DistinctUntilChanged'只允許一次搜索。當我更改文本並嘗試再次搜索時,observable不會加載數據。 – Enigmativity 2010-10-22 06:18:07

+0

爲什麼在我的例子中observable只能產生1個值的唯一原因是從「click」出來的「c」總是相同的。你是否完全按照我的回覆中所示的方式嘗試了代碼? – 2010-10-22 13:51:01

+0

有幾分鐘時間來編譯代碼(請參閱:http://s3.amazonaws.com/silverlight/DistinctUntilChanged.html)正如您所看到的那樣,它按預期工作(例如,每次用戶單擊按鈕時加載源代碼) – 2010-10-22 19:40:59