次の方法で共有


Populating Windows Phone view models with Rx

I’ve gotten rather fond of using the Reactive Extensions for populating my data from the web in my Windows Phone applications. They provide a lot of flexibility and the code stays in a readable form.

To demonstrate in a traditional MVVM model, let’s drop some movie information from Netflix onto our Windows Phone screen. First we need a structure to hold that movie data.

  1. public struct MovieData
  2. {
  3.     public string Name { get; set; }
  4.     public string Description { get; set; }
  5. }

We also need a view model for the view to bind to. The view model will expose two interesting properties for the the UI use. The NetworkBusy property can be bound to a progress bar to automatically show/hide the bar while the underlying network service is busy. The Movies property can be bound to a listbox so that the user can scroll through the list.

  1. public ObservableCollection<MovieData> Movies { get; set; }
  2.  
  3. private bool _networkBusy = false;
  4. public bool NetworkBusy {
  5.     get { return _networkBusy; }
  6.     set {
  7.         if (_networkBusy != value)
  8.         {
  9.             _networkBusy = value;
  10.             RaisePropertyChangedNotification("NetworkBusy");
  11.         }
  12.     }
  13. }

 

If our service class provides the view model with an IObservable<MovieData> when we ask for all the movies, we can just subscribe to the stream of MovieData objects it provides and add them to our Movies collection. Furthermore, since an Observable sequence provides error and completion notifications, we will know exactly when to disable the progress bar the UI uses by setting NetworkBusy to false. We can also take action on errors as needed. So we’d like it to be as simple as this.

  1. public void LoadData()
  2. {
  3.     NetworkBusy = true;
  4.  
  5.     Movies.Clear();
  6.  
  7.     var svc = new MovieService();
  8.     var results = svc.GetMovies();
  9.  
  10.     results.ObserveOnDispatcher().Subscribe(
  11.                 item => Movies.Add(item),
  12.                 ex => { LogException(ex); NetworkBusy = false; },
  13.                 () => NetworkBusy = false
  14.             );
  15. }

Also, note that the only thing we need to do to avoid accessing the UI on the wrong thread is to add ObserveOnDispatcher() to the mix.

Next we need to build the method on the MovieService class that will generate the Observable stream of MovieData objects for the view model. In this case we will return all the films that have a rating greater than 4. Here is the code for the method. I’ll break it down after the snippet.

  1. public IObservable<MovieData> GetData()
  2. {
  3.     // create the observable for the download event
  4.     WebClient wc = new WebClient();
  5.     var o = Observable.FromEvent<DownloadStringCompletedEventArgs>(wc, "DownloadStringCompleted");
  6.  
  7.     // initiate the transfer
  8.     wc.DownloadStringAsync(new Uri("https://odata.netflix.com/Catalog/Titles?$filter=AverageRating gt 4"));
  9.  
  10.     // convert string to data we are interested in
  11.     var results = o.Take(1).SelectMany(item =>
  12.     {
  13.         // use LINQ to XML to project the XML nodes
  14.         // to a collection of MovieData objects
  15.         var doc = XDocument.Parse(item.EventArgs.Result);
  16.         var movies = from movie in doc.Descendants(XName.Get("entry","https://www.w3.org/2005/Atom"))
  17.                      select new MovieData
  18.                      {
  19.                          Name = movie.Element(XName.Get("title", "https://www.w3.org/2005/Atom")).Value,
  20.                          Description = movie.Element(XName.Get("summary", "https://www.w3.org/2005/Atom")).Value
  21.                      };
  22.         return movies;
  23.     });
  24.  
  25.     return results;

Lines 4-5: create the WebClient and subscribe to the event in a manner that provides us an Observable. Because this wraps the even, ‘o’ is of type IObservable<DownloadStringCompletedEventArgs>

Once the download is initiated we provide some filtering and projection to the current sequence to transform it to an IObservable<MovieData> and return that to the user. The consumer doesn’t want to deal with EventArgs, he wants MovieData!

Line 11: Take(1) - Normally observables wrapped around events never complete. Adding Take(1) provides us the completion event because we know the event will only be raised once.  1 web request = 1 web response.

Line 11: SelectMany(…) – The single response of the web request is a string that contains multiple movies. If we just did Select() and returned a collection of MovieData objects, the consumer would have to loop over collection. If we use SelectMany(), the collection gets flattened out and we will return just the MovieData objects in the sequence.

Lines 15-21: take the resulting downloaded string and transform it with LINQ to XML into a collection of MovieData objects

Additional Fun: This on its face is a nice enough pattern, but there are a couple of others patterns which can emerge nicely on top of this one.

  • If you create multiple requests at the same time you can easily compose an observable from all of them in order to determine when all of them are complete.
  • With a single stream, you can create multiple sub streams to populate things like “first 10 items” on your front page or create filtered lists for pivots very easily.
  • If you leave out the Take(1) you leave open the possibility to reuse the WebClient again and continue to push additional pages of data back into the view model without the view model needing to be aware of the paging going on – its just waiting for a stream of MovieData’s

PhoneApp1.zip