So far, the weather application that you have created
is sending as many requests for weather data as the user types in zip
codes. When the data comes back from the weather web service, the order
that this data comes back in is not guaranteed. For example, if the user
first types in "32207" (Jacksonville) and then types in "10001" (New
York City), the weather results for Jacksonville may come in behind New
York City, yet the user would not realize that she's seeing
Jacksonville's weather when New York's zip code still remains on the
screen. It would be great if there were a solution that gave an
application the power to cancel out all weather requests that occurred
prior to the latest one, i.e., in our example, a request for
Jacksonville weather is canceled as soon as request for New York City
weather is made.
Rx.NET provides such a solution. There are two operators in Rx.NET—TakeUntil() and Switch—that
allow for cancellation of operations that occur prior to the latest
operation and are still "in-flight" so to speak, or are still pending
the return values. Through the use of an elegant LINQ query, these
operators tie together Observable collections, as you will see shortly.
But first, there is some bad news: in the current implementation of .NET
framework on Windows Phone 7, it is impossible to link the beginning of
the asynchronous web service invocation to the end of that invocation.
The root of the problem is the exclusion of the CreateChannel
method implementation in the Windows Communication Foundation libraries
on Windows Phone 7. Microsoft had to slim down and optimize .NET
framework on the phone, and the loss of this method for the time being
seems to be due to those optimization efforts.
Nevertheless, the
technique for canceling "in-flight" requests still applies to the
clients with full.NET framework installed (Windows Forms and WPF
applications) and to the Silverlight platform. For the weather
application, we will demonstrate the technique of canceling those
requests by creating a new Observable collection for the weather service each time a user types in a new zip code. Note, however, that the Observable
subscriptions that you will be creating listen for any completed
weather service requests, and not the specific ones. In other words,
each one of these subscriptions would process both Jacksonville and New
York City weather from the example, and the order that this weather data
comes in would be irrelevant. This is due to the limitation that we
have discussed in the current implementation of Windows Phone 7
framework—at present, you cannot link the beginning of the web service
call to the end of that service call on this platform.
To make the cancellation of operations on the Observable collections possible while those operations are "in-flight," you will change the code around to expose Observable collections to LINQ queries.
Follow these steps to make operation cancellation possible:
At the top of the MainPage
class (right above the constructor), paste the following code to
declare a module-level Observable collection for the KeyUp events of the
zip code text box:
IObservable<IEvent<KeyEventArgs>> _keys;
Expose the Observables for both the KeyUp event of the zip code text box and for the web service callback by adding the following two methods to your code:
private IObservable<IEvent<GetWeatherByZipCodeCompletedEventArgs>> GetWeatherSubject()
{
return Observable.FromEvent<svcWeather.GetWeatherByZipCodeCompletedEventArgs>(weatherClient, "GetWeatherByZipCodeCompleted");
}
private void GetKeys()
{
if (_keys == null)
{
_keys = Observable.FromEvent<KeyEventArgs>(txtZipCode, "KeyUp").Throttle(TimeSpan.FromSeconds(1)).DistinctUntilChanged();
}
}
The magic that makes the
cancellations work appears in the next code snippet. Pay particularly
close attention to the LINQ query; it establishes the relationship
between the Observable collection for the KeyUp events and the Observable
collection for the web service callbacks. Note that had Windows Phone 7
framework supported what is referred to as the Asynchronous pattern for
web service calls (with the use of BeginXXX/EndXXX
methods), you could have established a direct relationship between key
sequences and web service invocations. However, with the following code,
you have only a loose or indirect relationship between those two, since
each subscriptionlistens for any and all responses from the weather web
service, and not just for specific ones. Right after the LINQ
statement, there is a Switch() operator
that instructs the application to dispose of the old subscription to the
weather web service once there is a new key sequence awaiting in the _keys Observable collection.
Add the following code to the application:
private void WireUpWeatherEvents()
{
GetKeys();
var latestWeather = (from term in _keys
select GetWeatherSubject()
.Finally(() =>
{
Deployment.Current.Dispatcher.BeginInvoke(() => Debug.WriteLine("Disposed of prior subscription"));
})
).Switch();
latestWeather.ObserveOnDispatcher()
.Subscribe(evt =>
{
if (evt.EventArgs.Result != null)
{
lblWeatherFahrenheit.Text = "Current Weather, Fahrenheit: " + evt.EventArgs.Result.Details[0].MinTemperatureF.ToString() + " - " + evt.EventArgs.Result.Details[0].MaxTemperatureF.ToString();
lblCelsius.Text = "Current Weather, Celsius: " + evt.EventArgs.Result.Details[0].MinTemperatureC.ToString() + " - " + evt.EventArgs.Result.Details[0].MaxTemperatureC.ToString();
imgWeather.Source = new BitmapImage(new
Uri(evt.EventArgs.Result.Details[0].WeatherImage, UriKind.Absolute));
}
},
ex => {
Deployment.Current.Dispatcher.BeginInvoke(() =>lblStatus.Text =
ex.Message);
}
);
}
Notice the .Finally
statement in the code. Its purpose is to print a "Disposed of prior
subscription" message into the Output windows when one Observable
collection is being removed and replaced with the newer one. That occurs
when there is a new event in the _keys module-level Observable collection.
Finally, you need to make some minor changes to the WireUpKeyEvents function, namely, the Observable sequence generation for the KeyUp event on the zip code has been moved into a separate GetKeys method.
Replace the WiredUpKeyEvents() function with the following code:
private void WireUpKeyEvents()
{
GetKeys();
_keys.ObserveOn(Deployment.Current.Dispatcher).Subscribe(evt =>
{
if (txtZipCode.Text.Length >= 5)
{
weatherClient.GetWeatherByZipCodeAsync(txtZipCode.Text);
}
});
}
You are now ready to run the application.
Press
F5 and observe that the application behavior is virtually unchanged
from the previous walkthroughs: you still type in the zip code and
receive weather information for that zip code. However, behind the
scenes, you will notice the messages printed in the Output window
indicating that there are Observable
sequences being disposed of in accordance to the new data (zip codes
typed in) available in the key sequence observable collection.
Perhaps in the very near future, you will see a CreateChannel
method available on the Windows Phone 7 platform. Once that happens,
you could very easily enhance the foregoing walkthrough with the code
linking the beginning and end of an asynchronous web service call
through the Observable.FromAsyncPattern
method. For right now, however, you can still take advantage of this
extremely powerful feature of Rx.NET in Silverlight or on clients
running the full version of .NET framework.