There are a number of ways to retrieve data from a web service. Silverlight provides this functionality through:
- WebClient, which uses an eventing interface.
- HttpWebRequest, which uses asynchronous operations.
- Windows Communications Foundation (WCF), which uses a customizable framework.
The Reactive Extensions for .NET (Rx) builds upon this by providing functionality that allows for composing asynchronous and event-based applications. The MVVMGrid application uses the Rx extensions for the grid services and in certain event handling. I’ll focus on the grid services first.
An application service makes a call out to a web service and provides a response to the application. It must also provide some mechanism to notify the application that a service has failed for some reason. In the MVVMGrid application, the GridService class exposes its methods using IObservable collections. So instead of using an event or an asynchronous interface, a client to the service simply calls the Subscribe method to obtain the response from the service or a failure status.
Let’s examine the grid data service. The grid’s data is generated randomly by the application, but could have easily been retrieved from a web service.
public IObservable<GridDataCollection> GetGridItems() { return Observable .Repeat( 0, 1 ) .Select( _ => { GridDataCollection gridDataCollection = new GridDataCollection(); gridDataCollection.AddRandomData( 100 ); return gridDataCollection; } ) .SubscribeOn( Scheduler.ThreadPool ); }
Here
- The integer 0 is repeated once in order to get an IObservable collection started.
- A GridDataCollection object is created, 100 randomly-generated GridData items are inserted into it, and then it is selected.
- The subscription is moved to a worker thread so that the generation of these elements does not occur on the UI thread (which could make the Silverlight application less responsive).
In the GridViewModel, the OnImportsSatisfied method is used to initiate the process of obtaining data from the services.
private void SetExceptionMessage( Exception ex ) { ErrorMessageOne = String.Concat( "Error: ", ex.GetType().FullName ); ErrorMessageTwo = ex.Message; } public void OnImportsSatisfied() { ... Service .GetGridItems() .ObserveOnDispatcher() .Subscribe( d => { DataItems = new ObservableCollection<GridData>( d ); }, SetExceptionMessage ); }
The GetGridItems method of the service object returns an IObservable collection. Before subscribing, I ensure that the subscription handlers will be invoked on the UI thread (since I will be changing user interface elements). Finally, I subscribe to the IObservable interface and provide it two of the three possible handlers for handling the response: the OnNext handler and the OnError handler (the OnCompleted handler is not required in this case). On a successful response, the DataItems property on the GridViewModel is populated with the items to display. On an error response, I simply set the error message properties which will, in turn, be displayed below the grid.
Now let’s look at a more complicated example.
The “views” for the data grid are defined in XML files located on the web server. The GridService class is used to fetch both the list of all views as well as the individual views.
private const string ListFileName = "/Data/Settings.xml"; private readonly Uri WebSiteUri = HtmlPage.Document.DocumentUri; public IObservable<ViewSettingsCollection> GetViewSettingsCollection() { Uri listFileUri = new Uri( WebSiteUri, ListFileName ); WebRequest httpReq = HttpWebRequest.Create( listFileUri ); return Observable .FromAsyncPattern<WebResponse>( httpReq.BeginGetResponse, httpReq.EndGetResponse )() .Select( r => { XDocument rspDoc; using( Stream rspStream = r.GetResponseStream() ) rspDoc = XDocument.Load( rspStream ); return rspDoc .Element( "settings" ) .Elements( "items" ) .Select( items => new ViewSettingsCollection { DefaultViewSettingsID = (int?)items.Attribute( "default" ), Items = new List<ViewSettingsCollectionItem>( items .Elements( "item" ) .Select( item => new ViewSettingsCollectionItem { ID = (int)item.Attribute( "id" ), Description = (string)item.Attribute( "desc" ), Link = (string)item.Attribute( "link" ) }) ) }) .Single(); } ); }
An HttpWebRequest object is used to fetch the XML file from the Data folder on the web server. The FromAsyncPattern method is used to wrap the BeginGetResponse and EndGetResponse methods of the request object. When the response is received, LINQ queries are used to create a new ViewSettingsCollection object. The new instance of the ViewSettingsCollection object is then selected for the observable collection.
Back in the GridViewModel, the service is called to fetch the ViewSettingsCollection object.
Service .GetViewSettingsCollection() .ObserveOnDispatcher() .Subscribe( vsc => { SettingsCollection = vsc; SelectedSettings = SettingsCollection.GetDefaultSettings(); }, SetExceptionMessage );
Again, the subscription handlers will be invoked on the UI thread and the OnNext handler will simply set properties on the GridViewModel. Notice that the SelectedSettings property is also set. When this property is changed, either here or when the user changes the selected view in the drop-down box, the setter for this property calls the grid service to get the details of the view.
public ViewSettingsCollectionItem SelectedSettings { get { return selectedSettings; } set { selectedSettings = value; OnSelectedSettingsChanged(); OnPropertyChanged( "SelectedSettings" ); } } ... private void OnSelectedSettingsChanged() { if( SelectedSettings != null ) { Service .GetViewSettings( SelectedSettings ) .ObserveOnDispatcher() .Subscribe( vs => { ViewSettings = vs; }, SetExceptionMessage ); } }
This allows us to set up the columns to display in the data grid. I’ll examine this issue next.