In this third post in the series on implementing a streaming data provider, we show how to use the OData client library for Windows Phone 7 to asynchronously access binary data exposed by an Open Data Protocol (OData) feed. We also show how to asynchronously upload binary data to the data service. This Windows Phone sample is the asynchronous equivalent to the previous post Data Services Streaming Provider Series-Part 2: Accessing a Media Resource Stream from the Client; both client samples access the streaming provider that we create in the first blog post in this series: Implementing a Streaming Provider. This post also assumes that you are already somewhat familiar with using the OData client library for Windows Phone 7 (which you can obtain from the OData project in CodePlex), as well as phone-specific concepts like paged navigation and tombstoning. For more information about OData and Windows Phone, see the topic Open Data Protocol (OData) Overview for Windows Phone.
This application consumes an OData feed exposed by the sample photo data service, which implements a streaming provider to store and retrieve image files, along with information about each photo. This service returns a single feed (entity set) of PhotoInfo entries, which are also media link entries. The associated media resource for each media link entry is an image, which can be downloaded from the data service as a stream. The following represents the PhotoInfo entity in the data model:
This sample streaming data service is demonstrated in Implementing a Streaming Provider. You can download this streaming data service as a Visual Studio project from Streaming Photo OData Service Sample on MSDN Code Gallery. In our client phone application, we bind data from the PhotoInfo feed to UI controls in the XAML page.
First we need to create a Window Phone application that references the OData client library. (Note that the same basic APIs can be used to access and create media resources from a Silverlight client, except for the tombstoning functionality, which is specific to Windows Phone.) I won’t go into too much detail on the XAML that creates the pages in the application, since this is not a tutorial on XAML. You can review for yourself the XAML pages in the downloaded ODataStreamingPhoneClient project. Here are the basic steps to create this application:
DataSvcUtil.exe /out:"PhotoData.cs" /language:csharp /DataServiceCollection /uri:http://myhostserver/PhotoService/PhotoData.svc/ /version:2.0
The following steps are required to asynchronously query the streaming OData service. All code that access the OData service is implemented in the MainViewModel class.
// Declare the service root URI. private Uri svcRootUri = new Uri(serviceUriString, UriKind.Absolute); // Declare our private binding collection. private DataServiceCollection<PhotoInfo> _photos; // Declare our private DataServiceContext. private PhotoDataContainer _context; public bool IsDataLoaded { get; private set; }
// Declare the service root URI. private Uri svcRootUri = new Uri(serviceUriString, UriKind.Absolute);
// Declare our private binding collection. private DataServiceCollection<PhotoInfo> _photos;
// Declare our private DataServiceContext. private PhotoDataContainer _context;
public bool IsDataLoaded { get; private set; }
public DataServiceCollection<PhotoInfo> Photos { get { return _photos;} set { _photos = value; NotifyPropertyChanged("Photos"); // Register a handler for the LoadCompleted event. _photos.LoadCompleted += new EventHandler<LoadCompletedEventArgs>(Photos_LoadCompleted); } }
public DataServiceCollection<PhotoInfo> Photos { get { return _photos;} set { _photos = value;
NotifyPropertyChanged("Photos");
// Register a handler for the LoadCompleted event. _photos.LoadCompleted += new EventHandler<LoadCompletedEventArgs>(Photos_LoadCompleted); } }
// Instantiate the context and binding collection. _context = new PhotoDataContainer(svcRootUri); Photos = new DataServiceCollection<PhotoInfo>(_context); // Load the data from the PhotoInfo feed. Photos.LoadAsync(new Uri("/PhotoInfo", UriKind.Relative));
// Instantiate the context and binding collection. _context = new PhotoDataContainer(svcRootUri); Photos = new DataServiceCollection<PhotoInfo>(_context);
// Load the data from the PhotoInfo feed. Photos.LoadAsync(new Uri("/PhotoInfo", UriKind.Relative));
private void Photos_LoadCompleted(object sender, LoadCompletedEventArgs e) { if (e.Error == null) { // Make sure that we load all pages of the Customers feed. if (_photos.Continuation != null) { // Request the next page from the data service. _photos.LoadNextPartialSetAsync(); } else { // All pages are loaded. IsDataLoaded = true; } } else { if (MessageBox.Show(e.Error.Message, "Retry request?", MessageBoxButton.OKCancel) == MessageBoxResult.OK) { this.LoadData(); } } }
This sample displays images in the MainPage by binding a ListBox control to the Photos property of the ViewModel, which returns the binding collection containing data from the returned PhotoInfo feed. There are two ways to bind media resources from our streaming data service to the Image control.
Both of these approaches end up calling GetReadStreamUri on the context to return the URI of the media resource a specific PhotoInfo object, which is called the read stream URI. We ended-up going with the extension property approach, which is rather simple and ends up looking like this:
public partial class PhotoInfo { // Returns the media resource URI for binding. public Uri StreamUri { get { return App.ViewModel.GetReadStreamUri(this); } } }
When you bind an Image control using the read stream URI, the runtime does the work of asynchronously downloading the media resource during binding. The following XAML shows this binding to the StreamUri extension property for the image source:
<ListBox Margin="0,0,-12,0" Name="PhotosListBox" ItemsSource="{Binding Photos}" SelectionChanged="OnSelectionChanged" Height="Auto"> <ListBox.ItemsPanel> <ItemsPanelTemplate> <toolkit:WrapPanel ItemHeight="150" ItemWidth="150"/> </ItemsPanelTemplate> </ListBox.ItemsPanel> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Margin="0,0,0,17" Orientation="Vertical" HorizontalAlignment="Center"> <Image Source="{Binding Path=StreamUri, Mode=OneWay}" Height="100" Width="130" /> <TextBlock Text="{Binding Path=FileName, Mode=OneWay}" HorizontalAlignment="Center" Width="Auto" Style="{StaticResource PhoneTextNormalStyle}"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
Because the PhotoInfo class now includes the StreamUri extension property, the client also serializes this property in POST requests that create new media link entries in the data service. This causes an error in the data service when this unknown property cannot be processed. In our sample, we had to rewrite our requests to remove the StreamUri property from the request body. This payload rewriting is performed in the PhotoDataContainer partial class (defined in project file PhotoDataContainer.cs), which follows the basic pattern described in this post. I cover this and other binding issues related to media resource streams in more detail in my blog.
The following steps are required to create a new PhotoInfo entity and binary image file in the data service.
// Create a new PhotoInfo instance. PhotoInfo newPhoto = PhotoInfo.CreatePhotoInfo(0, string.Empty, DateTime.Now, new Exposure(), new Dimensions(), DateTime.Now); // Add the new photo to the tracking collection. App.ViewModel.Photos.Add(newPhoto); // Select the newly added photo. this.PhotosListBox.SelectedItem = newPhoto;
// Create a new PhotoInfo instance. PhotoInfo newPhoto = PhotoInfo.CreatePhotoInfo(0, string.Empty, DateTime.Now, new Exposure(), new Dimensions(), DateTime.Now);
// Add the new photo to the tracking collection. App.ViewModel.Photos.Add(newPhoto);
// Select the newly added photo. this.PhotosListBox.SelectedItem = newPhoto;
var selector = (Selector)sender; if (selector.SelectedIndex == -1) { return; } // Navigate to the details page for the selected item. this.NavigationService.Navigate( new Uri("/PhotoDetailsPage.xaml?selectedIndex=" + selector.SelectedIndex, UriKind.Relative)); selector.SelectedIndex = -1;
var selector = (Selector)sender; if (selector.SelectedIndex == -1) { return; }
// Navigate to the details page for the selected item. this.NavigationService.Navigate( new Uri("/PhotoDetailsPage.xaml?selectedIndex=" + selector.SelectedIndex, UriKind.Relative));
selector.SelectedIndex = -1;
if (chooserCancelled == true) { // The user did not choose a photo so return to the main page; // the added PhotoInfo is already removed. NavigationService.GoBack(); // Void out the binding so that we don't try and bind // to an empty PhotoInfo object. this.DataContext = null; return; } // Get the selected PhotoInfo object. string indexAsString = this.NavigationContext.QueryString["selectedIndex"]; int index = int.Parse(indexAsString); this.DataContext = currentPhoto = (PhotoInfo)App.ViewModel.Photos[index]; // If this is a new photo, we need to get the image file. if (currentPhoto.PhotoId == 0 && currentPhoto.FileName == string.Empty) { // Call the OnSelectPhoto method to open the chooser. this.OnSelectPhoto(this, new EventArgs()); }
if (chooserCancelled == true) { // The user did not choose a photo so return to the main page; // the added PhotoInfo is already removed. NavigationService.GoBack();
// Void out the binding so that we don't try and bind // to an empty PhotoInfo object. this.DataContext = null;
return; }
// Get the selected PhotoInfo object. string indexAsString = this.NavigationContext.QueryString["selectedIndex"]; int index = int.Parse(indexAsString); this.DataContext = currentPhoto = (PhotoInfo)App.ViewModel.Photos[index];
// If this is a new photo, we need to get the image file. if (currentPhoto.PhotoId == 0 && currentPhoto.FileName == string.Empty) { // Call the OnSelectPhoto method to open the chooser. this.OnSelectPhoto(this, new EventArgs()); }
// Initialize the PhotoChooserTask and assign the Completed handler. photoChooser = new PhotoChooserTask(); photoChooser.Completed += new EventHandler<PhotoResult>(photoChooserTask_Completed);
// Start the PhotoChooser. photoChooser.Show();
// Get back the last PhotoInfo objcet in the collection, // which we just added. currentPhoto = App.ViewModel.Photos[App.ViewModel.Photos.Count - 1]; if (e.TaskResult == TaskResult.OK) { // Set the file properties for the returned image. currentPhoto.FileName = GetFileNameFromString(e.OriginalFileName); currentPhoto.ContentType = GetContentTypeFromFileName(currentPhoto.FileName); // Read remaining entity properties from the stream itself. currentPhoto.FileSize = (int)e.ChosenPhoto.Length; // Create a new image using the returned memory stream. BitmapImage imageFromStream = new System.Windows.Media.Imaging.BitmapImage(); imageFromStream.SetSource(e.ChosenPhoto); // Set the height and width of the image. currentPhoto.Dimensions.Height = (short?)imageFromStream.PixelHeight; currentPhoto.Dimensions.Width = (short?)imageFromStream.PixelWidth; this.PhotoImage.Source = imageFromStream; // Reset the stream before we pass it to the service. e.ChosenPhoto.Position = 0; // Set the save stream for the selected photo stream. App.ViewModel.SetSaveStream(currentPhoto, e.ChosenPhoto, true, currentPhoto.ContentType, currentPhoto.FileName); } else { // Assume that the select photo operation was cancelled, // remove the added PhotoInfo and navigate back to the main page. App.ViewModel.Photos.Remove(currentPhoto); chooserCancelled = true; }
// Get back the last PhotoInfo objcet in the collection, // which we just added. currentPhoto = App.ViewModel.Photos[App.ViewModel.Photos.Count - 1];
if (e.TaskResult == TaskResult.OK) { // Set the file properties for the returned image. currentPhoto.FileName = GetFileNameFromString(e.OriginalFileName); currentPhoto.ContentType = GetContentTypeFromFileName(currentPhoto.FileName);
// Read remaining entity properties from the stream itself. currentPhoto.FileSize = (int)e.ChosenPhoto.Length;
// Create a new image using the returned memory stream. BitmapImage imageFromStream = new System.Windows.Media.Imaging.BitmapImage(); imageFromStream.SetSource(e.ChosenPhoto);
// Set the height and width of the image. currentPhoto.Dimensions.Height = (short?)imageFromStream.PixelHeight; currentPhoto.Dimensions.Width = (short?)imageFromStream.PixelWidth;
this.PhotoImage.Source = imageFromStream;
// Reset the stream before we pass it to the service. e.ChosenPhoto.Position = 0;
// Set the save stream for the selected photo stream. App.ViewModel.SetSaveStream(currentPhoto, e.ChosenPhoto, true, currentPhoto.ContentType, currentPhoto.FileName); } else { // Assume that the select photo operation was cancelled, // remove the added PhotoInfo and navigate back to the main page. App.ViewModel.Photos.Remove(currentPhoto); chooserCancelled = true; }
App.ViewModel.SaveChangesCompleted += new MainViewModel.SaveChangesCompletedEventHandler(ViewModel_SaveChangesCompleted); App.ViewModel.SaveChanges(); // Show the progress bar during the request. this.requestProgress.Visibility = Visibility.Visible; this.requestProgress.IsIndeterminate = true;
App.ViewModel.SaveChangesCompleted += new MainViewModel.SaveChangesCompletedEventHandler(ViewModel_SaveChangesCompleted);
App.ViewModel.SaveChanges();
// Show the progress bar during the request. this.requestProgress.Visibility = Visibility.Visible; this.requestProgress.IsIndeterminate = true;
// Send an insert or update request to the data service. this._context.BeginSaveChanges(OnSaveChangesCompleted, null);
private void OnSaveChangesCompleted(IAsyncResult result) { EntityDescriptor entity = null; // Use the Dispatcher to ensure that the response is // marshaled back to the UI thread. Deployment.Current.Dispatcher.BeginInvoke(() => { try { // Complete the save changes operation and display the response. DataServiceResponse response = _context.EndSaveChanges(result); // When we issue a POST request, the photo ID and // edit-media link are not updated on the client (a bug), // so we need to get the server values. if (response != null && response.Count() > 0) { var operation = response.FirstOrDefault() as ChangeOperationResponse; entity = operation.Descriptor as EntityDescriptor; var changedPhoto = entity.Entity as PhotoInfo; if (changedPhoto != null && changedPhoto.PhotoId == 0) { // Verify that the entity was created correctly. if (entity != null && entity.EditLink != null) { // Detach the new entity from the context. _context.Detach(entity.Entity); // Get the updated entity from the service. _context.BeginExecute<PhotoInfo>(entity.EditLink, OnExecuteCompleted, null); } } else { // Raise the SaveChangesCompleted event. if (SaveChangesCompleted != null) { SaveChangesCompleted(this, new AsyncCompletedEventArgs()); } } } } catch (DataServiceRequestException ex) { // Display the error from the response. MessageBox.Show(ex.Message); } catch (InvalidOperationException ex) { MessageBox.Show(ex.GetBaseException().Message); } }); }
private void OnSaveChangesCompleted(IAsyncResult result) { EntityDescriptor entity = null; // Use the Dispatcher to ensure that the response is // marshaled back to the UI thread. Deployment.Current.Dispatcher.BeginInvoke(() => { try { // Complete the save changes operation and display the response. DataServiceResponse response = _context.EndSaveChanges(result);
// When we issue a POST request, the photo ID and // edit-media link are not updated on the client (a bug), // so we need to get the server values. if (response != null && response.Count() > 0) { var operation = response.FirstOrDefault() as ChangeOperationResponse; entity = operation.Descriptor as EntityDescriptor;
var changedPhoto = entity.Entity as PhotoInfo;
if (changedPhoto != null && changedPhoto.PhotoId == 0) { // Verify that the entity was created correctly. if (entity != null && entity.EditLink != null) { // Detach the new entity from the context. _context.Detach(entity.Entity);
// Get the updated entity from the service. _context.BeginExecute<PhotoInfo>(entity.EditLink, OnExecuteCompleted, null); } } else { // Raise the SaveChangesCompleted event. if (SaveChangesCompleted != null) { SaveChangesCompleted(this, new AsyncCompletedEventArgs()); } } } } catch (DataServiceRequestException ex) { // Display the error from the response. MessageBox.Show(ex.Message); } catch (InvalidOperationException ex) { MessageBox.Show(ex.GetBaseException().Message); } }); }
private void OnExecuteCompleted(IAsyncResult result) { // Use the Dispatcher to ensure that the response is // marshaled back to the UI thread. Deployment.Current.Dispatcher.BeginInvoke(() => { try { // Complete the query by assigning the returned // entity, which materializes the new instance // and attaches it to the context. We also need to assign the // new entity in the collection to the returned instance. PhotoInfo entity = _photos[_photos.Count - 1] = _context.EndExecute<PhotoInfo>(result).FirstOrDefault(); // Report that that media resource URI is updated. entity.ReportStreamUriUpdated(); } catch (DataServiceQueryException ex) { MessageBox.Show(ex.Message); } finally { // Raise the event by using the () operator. if (SaveChangesCompleted != null) { SaveChangesCompleted(this, new AsyncCompletedEventArgs()); } } }); }
private void OnExecuteCompleted(IAsyncResult result) { // Use the Dispatcher to ensure that the response is // marshaled back to the UI thread. Deployment.Current.Dispatcher.BeginInvoke(() => { try { // Complete the query by assigning the returned // entity, which materializes the new instance // and attaches it to the context. We also need to assign the // new entity in the collection to the returned instance. PhotoInfo entity = _photos[_photos.Count - 1] = _context.EndExecute<PhotoInfo>(result).FirstOrDefault();
// Report that that media resource URI is updated. entity.ReportStreamUriUpdated(); } catch (DataServiceQueryException ex) { MessageBox.Show(ex.Message); } finally { // Raise the event by using the () operator. if (SaveChangesCompleted != null) { SaveChangesCompleted(this, new AsyncCompletedEventArgs()); } } }); }
// Hide the progress bar now that save changes operation is complete. this.requestProgress.Visibility = Visibility.Collapsed; this.requestProgress.IsIndeterminate = false; // Unregister for the SaveChangedCompleted event now that we are done. App.ViewModel.SaveChangesCompleted -= new MainViewModel.SaveChangesCompletedEventHandler(ViewModel_SaveChangesCompleted); NavigationService.GoBack();
// Hide the progress bar now that save changes operation is complete. this.requestProgress.Visibility = Visibility.Collapsed; this.requestProgress.IsIndeterminate = false;
// Unregister for the SaveChangedCompleted event now that we are done. App.ViewModel.SaveChangesCompleted -= new MainViewModel.SaveChangesCompletedEventHandler(ViewModel_SaveChangesCompleted); NavigationService.GoBack();
Senior Programming Writer WCF Data Services