Freigeben über


Accessing an OData Media Resource Stream from a Windows Phone 7 Application (Streaming Provider Series-Part 3)

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.

OData Client Programming for Windows Phone 7

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:

  1. Download and install the OData client library for Windows Phone 7. This includes the System.Data.Services.Client.dll assembly and the DataSvcUtil.exe tool.

  2. Create the Windows Phone project.

  3. Run the DataSvcUtil.exe program (included in the OData client library for Windows Phone 7 download) to generate the client data classes for the data service.
    Your command line should look like this (except all on one line):

    DataSvcUtil.exe /out:"PhotoData.cs" /language:csharp /DataServiceCollection
    /uri:https://myhostserver/PhotoService/PhotoData.svc/ /version:2.0

  4. Add a reference to the System.Data.Services.Client.dll assembly.

  5. Create a ViewModel class for the application named MainViewModel. This ViewModel helps connect the view (controls in XAML pages) to the model (OData feed accessed using the client library) by exposing properties and methods required for data binding and tombstoning. The following represents the MainViewModel class that supports this sample:
    image

  6. Implement tombstoning to store application state when the application is deactivated and restore state when the application is reactivated. This is important because deactivation can happen at any time, including when the application itself displays the PhotoChooserTask to select a photo stored on the phone. To learn more about how to tombstone using the DataServiceState object, see Open Data Protocol (OData) Overview for Windows Phone.

  7. The MainPage.xaml page displays a ListBox of PhotoInfo objects, which includes the media resources as images downloaded from the streaming data service.
    image

  8. When one of the items in the ListBox is tapped, details of the selected PhotoInfo object are displayed in a Pivot control the PhotoDetailsWindow page:
    imageimage 

Querying the Data Service and Binding the Streamed Data

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.

  1. Declare the DataServiceContext used to access the data service and the DataServiceCollection used for data binding.

    // 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; }

  2. Register a handler for the LoadCompleted event when the binding collection is 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);
    }
    }

  3. When MainPage.xaml is navigated to, the LoadData method on the ViewModel is called; the LoadAsync method asynchronously executes the query URI.

    // 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));

  4. The Photos_LoadCompleted method handles the LoadCompleted event to load all pages of the PhotoInfo feed returned by the data service.

    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();
    }
    }
    }

  5. When the user selects an image in the list, PhotoDetailsPage.xaml is navigated to, which displays data from the selected PhotoInfo object.

Binding Image Data to UI Controls

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.

  • By defining an extension property on the media link entry.
  • By implementing a value converter.

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.

Uploading a New Image to the Data Service

The following steps are required to create a new PhotoInfo entity and binary image file in the data service.

  1. When the user taps the CreatePhoto button to upload a new image, we must create a new MLE object on the client. We do this by calling DataServiceCollection.Add in the MainPagecode-behind page:

    // 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;

    In this case, we don’t need to call AddObject on the context because we are using a DataServiceCollection for data binding.

  2. When the new PhotoInfo is selected from the list, the following SelectionChanged handler is invoked:

    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;

    This navigates to the PhotoDetailsPage with the index of the newly created PhotoInfo object in the query parameter.

  3. In the code-behind page for the PhotoDetailsPage, the following method handles the OnNavigatedTo event: 

    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 we have a new PhotoInfo object (with a zero ID), the OnSelectedPhoto method is called. 

  4. In the PhotoDetailsPage, we must initialize the PhotoChooserTask in the class constructor: 

    // Initialize the PhotoChooserTask and assign the Completed handler.
    photoChooser = new PhotoChooserTask();
    photoChooser.Completed +=
    new EventHandler<PhotoResult>(photoChooserTask_Completed);

  5. In the OnSelectedPhoto method (which also handles the SelectPhoto button tap) we display the photo chooser: 

    // Start the PhotoChooser.
    photoChooser.Show();

    At this point, the PhotoChooserTask is displayed and the application itself is deactivated, to be reactivated when the chooser closes—hence the need to implement tombstoning. 

  6. When the photo chooser is closed, the Completed event is raised. When the application is fully reactivated, we handle the event as follows to set PhotoInfo properties based on the selected photo:

    // 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;
    }

    Note that we use the image stream to create a new BitmapImage, which is only used to automatically set the height and width properties of the image.

  7. When the Save button in the PhotoDetailsPage is tapped, we register a handler for the SaveChangesCompleted event in the ViewModel, start the progress bar, and call SaveChanges in the ViewModel: 

    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;

  8. In the ViewModel, we call BeginSaveChanges to send the media resource as a binary stream (along with any other pending PhotoInfo object updates) to the data service:

    // Send an insert or update request to the data service.
    this._context.BeginSaveChanges(OnSaveChangesCompleted, null);

    When BeginSaveChanges is called, the client sends a POST request to create the media resource in the data service using the supplied stream. After it processes the stream, the data service creates an empty media link entry. The client then sends a subsequent MERGE request to update this new PhotoInfo entity with data from the client.

  9. In the following callback method, we call the EndSaveChanges method to get the response to the POST request generated when BeginSaveChanges was called:

    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);
    }
    });
    }

    When creating a new photo, we also need need to execute a query to get the newly created media link entry from the data service, after first detaching the new entity. We must do this because of a limitation in the WCF Data Services client POST behavior where it does not update the object on the client with the server-generated values or the edit-media link URI. To get the updated entity materialized correctly from the data service, we first detach the new entity and then call BeginExecute to get the new media link entry.

  10. When we handle the callback from the subsequent query execution, we assign the returned object to a new instance to properly materialize the new media link entry:

    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());
    }
    }
    });
    }

    Because we detached the new media link entry, we must also assign the now tracked PhotoInfo object to the appropriate instance in the binding collection, otherwise the binding collection is out of sync with the context.

  11. Finally, the SaveChangesCompleted event is raised by the ViewModel, to inform the UI that it is OK to turn off the progress bar, which is handled in the following code in the PhotoDetailsPage:

    // 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();

    Unfortunately, when the navigation returns to the MainPage, the binding again downloads the images. This is because of the application deactivation that occurs when the PhotoChooserTask is displayed. To avoid this re-download from the data service after tombstoning, you could instead use the GetReadStream method to get a stream that contains the image data and use it to create an image in isolated storage. Then, your binding could access the stored version instead of the web version of the image, but this is outside the scope of this sample.

 

Glenn Gailey

Senior Programming Writer
WCF Data Services

Comments