September 8th, 2010

Data Services Streaming Provider Series-Part 2: Accessing a Media Resource Stream from the Client

In this second post in the series on implementing a streaming data provider, we show how to use the WCF Data Services client library to access binary data exposed by an Open Data Protocol (OData) feed, as well as has how to upload binary data to the data service. For more information on the streaming provider, see the first blog post in this series: Implementing a Streaming Provider.

PhotoData Sample Client Application

In the previous post, we showed how to implement IDataServiceStreamProvider to create a data service that uses OData to store and retrieve binary image files, the media resource (MR) in OData terms, along with metadata about the photos, the media link entry (MLE) in OData terms. The Visual Studio solution for this sample data service, which is published to this MSDN Code Gallery page, also contains a client application project. This client, which consumes feeds from the PhotoData service, accesses and displays image files stored by the data service. Creating a client application that can display photos from and send photos to our PhotoData sample data service requires the following basic steps:

  1. Create the client project and add a reference to the data service.
  2. Create the UI elements to display the MLE and MR data for the photo.
  3. Request a data feed from the service, get the URI of the MR for a specific MLE, and then use this URI to create an image based on the MR.
  4. When we are adding a new photo, create a new MLE object on the client, set any properties, and add the object to the DataServiceContext.
  5. Read an image from the local computer into a stream, pass the stream and the object to the SetSaveStream method, and then call SaveChanges.

As you may recall, the PhotoData sample data service exposes a single PhotoInfo entity (an MLE) which has a related image file (the MR). The HasStream attribute applied to the PhotoInfo entity tells the client that it is an MLE, as you can see in the PhotoInfo entity metadata returned by the data service:

Now, let’s get started on creating the client application.

Creating the WPF Client Application

Our client application is a Windows Presentation Foundation (WPF) application. This enables us to use data binding of PhotoInfo objects to UI elements.

First let’s create the WPF client application (I won’t go into too much detail here…you can review the XAML that defines these windows in the client project included with the sample PhotoData streaming data service):

  1. Create the WPF project.
  2. Use the Add Service Reference dialog in Visual Studio to add a reference to the PhotoData service that we implemented in the previous post.
  3. The PhotoWindow displays the FileName property of the PhotoInfo objects materialized from the data feed in the photoComboBox:

    image

    When one of the PhotoInfo objects is selected, by file name, from the photoComboBox, the related image MR is requested from the data service and displayed as an image in the page.

  4. The PhotoDetailsWindow displays the properties of the selected PhotoInfo object:

    image

    This window is displayed when you click Add Photo or Photo Details in the PhotoWindow.

Now let’s see how the MLE and MR data from the data service is propagated into these UI elements.

Querying the Data Service and Displaying the Streamed Data

The following steps are required to query the PhotoData service for the data feed and get images for a specific PhotoInfo object.

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

    private PhotoDataContainer context;
    private DataServiceCollection<PhotoInfo> trackedPhotos;
    private PhotoInfo currentPhoto;

    // Get the service URI from the settings file.
    private Uri svcUri =
        new Uri(Properties.Settings.Default.svcUri);

    Note that the data service URI is stored in the app.config file.

  2. When the PhotoWindow is loaded, execute the query that loads the binding collection with objects from the PhotoInfo feed returned by the data service:

    // Define a query that returns a feed with all PhotoInfo objects.
    var query = context.PhotoInfo;

    // Create a new collection for binding based on the executed query.
    trackedPhotos = new DataServiceCollection<PhotoInfo>(query);

    // Load all pages of the response into the binding collection.
    while (trackedPhotos.Continuation != null)
    {
        trackedPhotos.Load(
            context.Execute<PhotoInfo>(trackedPhotos.Continuation.NextLinkUri));
    }

  3. When the user selects a file name from the photoComboBox, call the GetReadStreamUri method to request the MR from the data service, as follows:

    // Use the ReadStreamUri of the Media Resource for selected PhotoInfo object
    // as the URI source of a new bitmap image.
    photoImage.Source = new BitmapImage(context.GetReadStreamUri(currentPhoto));

    The returned URI is used to create the image on the client that is displayed in the UI.

    Note: The URI of the MR works very well for creating an image on the client. However, when you need the MR returned as a binary stream instead of just the URI, you must instead use the GetReadStream method. This method returns a DataServiceStreamResponse object that contains the binary stream of MR data, accessible from the Stream property.

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 adding a new photo, we must create a new MLE object on the client. We do this by calling DataServiceCollection.Add in the PhotoWindow code-behind page:

    // Create a new PhotoInfo object.
    PhotoInfo newPhotoEntity = new PhotoInfo();

    // Ceate an new PhotoDetailsWindow instance with the current
    // context and the new photo entity.
    PhotoDetailsWindow addPhotoWindow =
        new PhotoDetailsWindow(newPhotoEntity, context);

    addPhotoWindow.Title = "Select a new photo to upload…";

    // We need to have the new entity tracked to be able to
    // call DataServiceContext.SetSaveStream.
    trackedPhotos.Add(newPhotoEntity);

    // If we successfully created the new image, then display it.
    if (addPhotoWindow.ShowDialog() == true)
    {
        // Set the index to the new photo.
        photoComboBox.SelectedItem = newPhotoEntity;
    }
    else
    {
        // Remove the new entity since the add operation failed.
        trackedPhotos.Remove(newPhotoEntity);
    }

    This code instantiates and displays the PhotoDetailsWindow to sets properties of the new PhotoInfo object and the binary stream. When we are updating an existing PhotoInfo object, we just pass the existing PhotoInfo object. In this case, we pass a new PhotoInfo object, but we don’t need to call AddObject on the context because we are using a DataServiceCollection for data binding.

  2. In the code-behind for the PhotoDetailsWindow, we use a FileStream to read an image from the local computer and pass this stream and the PhotoInfo object to the SetSaveStream method:

    // Create a dialog to select the image file to stream to the data service.
    Microsoft.Win32.OpenFileDialog openImage = new Microsoft.Win32.OpenFileDialog();
    openImage.FileName = "image";
    openImage.DefaultExt = ".*";
    openImage.Filter = "Images File|*.jpg;*.png;*.gif;*.bmp";
    openImage.Title = "Select the image file to upload…";
    openImage.Multiselect = false;
    openImage.CheckFileExists = true;

    // Reset the image stream.
    imageStream = null;

    try
    {
        if (openImage.ShowDialog(this) == true)
        {
            if (photoEntity.PhotoId == 0)
            {
                // Set the image name from the selected file.
                photoEntity.FileName = openImage.SafeFileName;

                photoEntity.DateAdded = DateTime.Today;

                // Set the content type and the file name for the slug header.
                photoEntity.ContentType =
                    GetContentTypeFromFileName(photoEntity.FileName);
            }

            photoEntity.DateModified = DateTime.Today;

            // Use a FileStream to open the existing image file.
            imageStream = new FileStream(openImage.FileName, FileMode.Open);

            photoEntity.FileSize = (int)imageStream.Length;

            // Create a new image using the memory stream.
            BitmapImage imageFromStream = new BitmapImage();
            imageFromStream.BeginInit();
            imageFromStream.StreamSource = imageStream;
            imageFromStream.CacheOption = BitmapCacheOption.OnLoad;
            imageFromStream.EndInit();

            // Set the height and width of the image.
            photoEntity.Dimensions.Height = (short?)imageFromStream.PixelHeight;
            photoEntity.Dimensions.Width = (short?)imageFromStream.PixelWidth;

            // Reset to the beginning of the stream before we pass it to the service.
            imageStream.Position = 0;

            // Set the file stream as the source of binary stream
            // to send to the data service. The Slug header is the file name and
            // the content type is determined from the file extension.
            // A value of ‘true’ means that the stream is closed by the client when
            // the upload is complete. 
            context.SetSaveStream(photoEntity, imageStream, true,
                photoEntity.ContentType, photoEntity.FileName);

            return true;
        }
        else
        {
            MessageBox.Show("The selected file could not be opened.");
            return false;
        }
    }
    catch (IOException ex)
    {
        MessageBox.Show(
            string.Format("The selected image file could not be opened. {0}",
            ex.Message), "Operation Failed");
        return false;
    }

                          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.

  3. When the savePhotoDetails button in the PhotoDetailsWindow is clicked, we call SaveChanges to send the MR as a binary stream (and any PhotoInfo object updates) to the data service:

    // Send the update (POST or MERGE) request to the data service and
    // capture the added or updated entity in the response.
    ChangeOperationResponse response =
        context.SaveChanges().FirstOrDefault() as ChangeOperationResponse;

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

  4. When uploading a new photo (POST), we also need need to execute a query to get the new media link entry by using a MergeOption.OverwriteChanges to update the client object instance to to get all of the data from the newly created MLE from the data service, as follows.

    // 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 (photoEntity.PhotoId == 0)
    {
        if (response != null)
        {
            entity = response.Descriptor as EntityDescriptor;
        }

        // Verify that the entity was created correctly.
        if (entity != null && entity.EditLink != null)
        {
            // Cache the current merge option (we reset to the cached
            // value in the finally block).
            cachedMergeOption = context.MergeOption;

            // Set the merge option so that server changes win.
            context.MergeOption = MergeOption.OverwriteChanges;

            // Get the updated entity from the service.
            // Note: we need Count() just to execute the query.
            context.Execute<PhotoInfo>(entity.EditLink).Count();
        }
    }

    We have to 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.

Glenn Gailey
Senior Programming Writer
WCF Data Services

Author

0 comments

Discussion are closed.