MULTIMEDIA

Silverlight Recipes : Networking and Web Service Integration - Accessing Resources over HTTP

5/5/2011 4:13:32 PM

1. Problem

You need to access resources located at a remote HTTP endpoint from your Silverlight application. You may need to read from or write to remote streams or have to download/upload resources over HTTP.

2. Solution

Use the WebClient API to read from or write to remote resources, or download or upload resources.

3. How It Works

The WebClient type has a convenient collection of methods that let you access resources over HTTP. You can use the WebClient class in two basic modes: uploading/downloading resources as strings and reading from or writing to streams, both over HTTP.

3.1. Downloading/Uploading Resources

You can use the DownloadStringAsync() method to asynchronously download any resource over HTTP as the long as the resource is (or can be converted to) a string. DownloadStringAsync() accepts a URI to the resource and raises the DownloadStringProgressChanged event to report download progress. Download completion is signaled when the DownloadStringCompleted event is raised. The DownloadStringCompletedEventArgs.Result property exposes the downloaded string resource.

The UploadStringAsync() method similarly accepts the upload endpoint URI. It also accepts the string resource to upload and reports completion by raising the UploadStringCompleted event.

Both methods accept a user-supplied state object, which is made available in the progress change and the completion event handlers through the UserState property on the DownloadProgressChangedEventArgs, DownloadStringCompletedEventArgs, or UploadStringCompletedeventArgs parameter.

3.2. Reading/Writing Remote Streams

The OpenReadAsync() method accepts a remote HTTP URI and attempts to download the resource and make it available as a locally readable stream. Download progress is reported using the DownloadProgressChanged event, as mentioned earlier. The completion of the asynchronous read is signaled by the runtime by raising the OpenReadCompleted event. In the handler for OpenReadCompleted, the OpenReadCompletedEventArgs.Result property exposes the resource stream.

The OpenWriteAsync() method behaves slightly differently. Before it tries to access the remote resource, it raises the OpenWriteCompleted event synchronously. In the handler for this event, you are expected to write to the OpenWriteCompletedEventArgs.Result stream the data you want to save to the remote resource. After this stream is written and closed, and the handler returns, the runtime attempts to asynchronously send the data to the remote endpoint.

3.3. WebClient and HTTP Endpoints

In previous recipes, we outline the use of the HttpWebRequest/HttpWebResponse APIs with POX- or JSON-enabled web services. Although the WebClient API is primarily meant for accessing remote resources, its DownloadStringAsync() and UploadStringAsync() APIs can be effectively used for similar web-service communication as well, where POX or JSON messages formatted as strings are exchanged using this API set. Additionally, WebClient can work with other HTTP endpoints such as ASP.NET web pages. The code samples use a mix of WCF services and ASP.NET pages to illustrate this.

3.4. Canceling Long-Running Operations

Depending on the size of the resource being accessed, the available network bandwidth, and similar factors, download operations can be long-running, and it is desirable to provide application users with a way to cancel an operation should they choose to do so. The WebClient type exposes a property called IsBusy, which when true indicates that the WebClient instance is currently performing a background operation. Calling CancelAsync() on a WebClient instance attempts to cancel any such running operation. Note that because the operation is on a background thread, if CancelAsync() succeeds, the completion handler is invoked on the main thread, just as it would be on a successful completion. In the handler, you can check the Cancelled property on the event argument parameter to see if the operation was canceled or if it was a normal completion.

We show the use of all these in the following sample for this recipe.

4. The Code

The sample used here implements a simple photo-management application. The UI for the application is shown in Figure 1.

Figure 1. The photo-management application UI

The application downloads a ZIP file on start and displays image thumbnails contained in the ZIP. When you select a specific thumbnail, the full-resolution image is downloaded. Additional custom metadata can be associated with the image and saved to the server. Clicking the Upload button allows the user to select and upload a locally available JPEG image file.

The back-end functionality is divided into three sets of operations related to metadata management, photo downloads, and photo uploads, and is implemented across two WCF services and a pair of ASP.NET pages.

Listing 1 shows the service and data contracts for the various services.

Listing 1. Service and data contracts for the WCF services in Contracts.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;

using System.ServiceModel;
using System.ServiceModel.Web;

namespace Recipe7_4.PhotoService
{
[ServiceContract]
public interface IPhotoDownload
{
[OperationContract]
[WebGet()]
//get the zip file containing the thumbnails
Stream GetThumbs();

[OperationContract]
[WebGet(UriTemplate = "Photos?Name={PhotoName}")]
//get a full resolution image
byte[] GetPhoto(string PhotoName);
}

[ServiceContract]
public interface IMetadata
{
[OperationContract]
[WebGet(ResponseFormat = WebMessageFormat.Json)]
//get the names of all the JPEG images available for download
List<string> GetPhotoFileNames();

[OperationContract]
[WebGet(UriTemplate = "PhotoMetadata?Id={PhotoId}",
ResponseFormat = WebMessageFormat.Json)]
//get the metadata for a specific image
PhotoMetaData GetPhotoMetaData(string PhotoId);

}

[DataContract]
public class PhotoMetaData
{
[DataMember]
public string Id { get; set; }
[DataMember]
public string Name { get; set; }
[DataMember]
public string Description { get; set; }
[DataMember]


public string Location { get; set; }
[DataMember]
public int? Rating { get; set; }
[DataMember]
public DateTime? DateTaken { get; set; }
}
}

The sample code for this recipe contains the full implementation of two WCF services, Metadata.svc and PhotoDownload.svc, that implement the IMetadata and IPhotoDownload contracts respectively, as shown in Listing 1.

Listing 2 shows the codebehind for MetadataUpload.aspx. The page markup contains nothing of relevance because the page does not render anything; it is used purely as an endpoint to which some data is posted by a WebClient instance.

Listing 2. MetadataUpload.aspx page codebehind in MetadataUpload.aspx.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization.Json;

namespace Recipe7_4.PhotoService
{
public partial class MetadataUpload : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (Request.HttpMethod == "POST")
{
DataContractJsonSerializer jsonSer =
new DataContractJsonSerializer(typeof(PhotoMetaData));
SetPhotoMetaData(
jsonSer.ReadObject(Request.InputStream) as PhotoMetaData);
Response.SuppressContent = true;
}
}

public void SetPhotoMetaData(PhotoMetaData MetaData)
{
PhotoStoreDataContext dcPhoto = new PhotoStoreDataContext();
List<PhotoData> pds = (from pd in dcPhoto.PhotoDatas
where pd.PhotoId == MetaData.Id
select pd).ToList();


if (pds.Count == 0)
{
dcPhoto.PhotoDatas.InsertOnSubmit(new PhotoData {
PhotoId = MetaData.Id, Name = MetaData.Name,
Location = MetaData.Location, DateTaken = MetaData.DateTaken,
Description = MetaData.Description, Rating = MetaData.Rating });
}
else
{
pds[0].Name = MetaData.Name;
pds[0].DateTaken = MetaData.DateTaken;
pds[0].Description = MetaData.Description;
pds[0].Location = MetaData.Location;
pds[0].Rating = MetaData.Rating;
}
dcPhoto.SubmitChanges();
}
}
}

As you can see in Listing 2, you check for an incoming POST request in the Page_Load handler of the ASPX page and deserialize the JSON stream into a PhotoMetadata object. You then pass the PhotoMetadata instance to SetPhotoMetadata(), which uses LINQ to SQL to update the database. Before you return from the Page_Load handler, you set Response.SuppressContent to true. This ensures that there is no HTML markup response from the page, because you need none.

Listing 3 shows the implementation of PhotoUpload.aspx, which is structured in a similar fashion.

Listing 3. PhotoUpload.aspx codebehind in PhotoUpload.aspx.cs
using System;
using System.IO;
using System.Web;

namespace Recipe7_4.PhotoService
{
public partial class PhotoUpload1 : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (Request.HttpMethod == "POST")
{
AddPhoto(Request.InputStream);
Response.SuppressContent = true;
}
}

public void AddPhoto(Stream PhotoStream)
{
//get the file name for the photo
string PhotoName =
HttpContext.Current.Request.Headers["Image-Name"];
if (PhotoName == null) return;
//open a file stream to store the photo
FileStream fs = new FileStream(
HttpContext.Current.Request.MapPath
(string.Format("APP_DATA/Photos/{0}", PhotoName)),
FileMode.Create, FileAccess.Write);
//read and store
BinaryReader br = new BinaryReader(PhotoStream);
BinaryWriter bw = new BinaryWriter(fs);

int ChunkSize = 1024 * 1024;
byte[] Chunk = null;
do
{
Chunk = br.ReadBytes(ChunkSize);
bw.Write(Chunk);
bw.Flush();
} while (Chunk.Length == ChunkSize);

br.Close();
bw.Close();
}
}
}


Note that the images and the ZIP file containing the thumbnails are stored on the server file system, under the App_Data folder of the ASP.NET web application hosting the WCF services. The metadata for each image, however, is stored in a SQL Server database. For the samples, we use SQL Server 2008 Express version, which you can download for free from www.microsoft.com/express/sql/download/default.aspx. When you install the product, take care to name the server SQLEXPRESS. This is the default name that the SQL 2008 installer uses, and so does the code sample. If you change it, visit the web.config files for the web service project named "7.4 PhotoService" in the sample code for this recipe, and change the database-connection strings to reflect your chosen server name. The following snippet shows the configuration entry in web.config:

<connectionStrings>
<add name="SLBook_recipe_7_4_dbConnectionString"
connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=Recipe_7_4_db;
Integrated Security=True" providerName="System.Data.SqlClient"/>
</connectionStrings>

After SQL 2008 is installed, you need to create a database named Recipe_4_7_db and run the Recipe_7_4_db.sql file included with the sample code to create the necessary data model. We also include a database backup file named Recipe_4_7_db.bak, which you can restore into your SQL 2008 instance in lieu of creating the database and running the queries yourself.

Listing 4 shows some of the data types used in the client application.

Listing 4. Data types used in the client application in DataTypes.cs
using System.ComponentModel;
using System.Runtime.Serialization;
using System.Windows;
using System.Windows.Media.Imaging;

namespace Recipe7_4.PhotoClient
{
public class WrappedImage : INotifyPropertyChanged
{
//bound to the thumbnail
public BitmapImage Small { get; set; }
//bound to the full res image
public BitmapImage Large { get; set; }
//Metadata
private PhotoMetaData _Info = null;
public PhotoMetaData Info
{
get { return _Info; }
set
{
_Info = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Info"));
}
}
//Download Progress
private double _PercentProgress;
public double PercentProgress
{
get { return _PercentProgress; }
set
{
_PercentProgress = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("PercentProgress"));
}



}
//show the progress bar
private Visibility _ProgressVisible = Visibility.Collapsed;
public Visibility ProgressVisible
{
get { return _ProgressVisible; }
set
{
_ProgressVisible = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("ProgressVisible"));
}
}
//parts removed for brevity

//download completed - show the image
private Visibility _ImageVisible = Visibility.Collapsed;
public Visibility ImageVisible
{
get { return _ImageVisible; }
set
{
_ImageVisible = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("ImageVisible"));
}
}
//name of the thumbnail file
private string _ThumbName;
public string ThumbName
{
get { return _ThumbName; }
set
{
_ThumbName = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("ThumbName"));
}
}
//name of the image file
private string _FileName;
public string FileName
{
get { return _FileName; }
set



{
_FileName = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("FileName"));
}
}

public event
PropertyChangedEventHandler PropertyChanged;

}
[DataContract]
public class PhotoMetaData : INotifyPropertyChanged
{
//a unique Id for the image file - the file name
private string _Id;
[DataMember]
public string Id
{
get { return _Id; }
set
{
_Id = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Id"));
}
}
//a user supplied friendly name
private string _Name;
[DataMember]
public string Name
{
get { return _Name; }
set
{
_Name = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Name"));
}
}
private string _Description;
[DataMember]
public string Description
{
get { return _Description; }



set
{
_Description = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Description"));
}
}
private string _Location;
[DataMember]
public string Location
{
get { return _Location; }
set
{
_Location = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Location"));
}
}
private int? _Rating;
[DataMember]
public int? Rating
{
get { return _Rating; }
set
{
_Rating = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Rating"));
}
}
private DateTime? _DateTaken;
[DataMember]
public DateTime? DateTaken
{
get { return _DateTaken; }
set
{
_DateTaken = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("DateTaken"));
}
}

public event PropertyChangedEventHandler PropertyChanged;


}
}

The WrappedImage type, as shown in Listing 4, is used to wrap an image and its metadata. It implements INotifyPropertyChange to facilitate data binding to XAML elements in the UI. For more about data binding and property-change notifications. The WrappedImage type contains individual BitmapImage instances for the thumbnail and the high-resolution image, and a few other properties that relate to download-progress reporting and visibility of different parts of the UI.

Also shown is the PhotoMetadata data-contract type used to transfer metadata to and from the WCF services. The difference between the client implementation of PhotoMetadata shown here and the one used in the service shown in Listing 1 is that you add property-change notification code to each property in the client-side implementation.

Listing 5 shows the XAML for MainPage. The XAML for this page is fairly extensive, so we discuss only pertinent portions briefly.

Listing 5. XAML for MainPage in MainPage.xaml.cs
<UserControl x:Class="Recipe7_4.PhotoClient.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
FontFamily="Trebuchet MS" FontSize="11"
Width="800" Height="700"
xmlns:Controls
="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows">
<UserControl.Resources>

<DataTemplate x:Key="dtProgressMessage">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Processing" Margin="0,0,5,0" Foreground="Red"/>
<TextBlock Text="{Binding}" Margin="0,0,2,0" Foreground="Red"/>
<TextBlock Text="%" Foreground="Red"/>
</StackPanel>
</DataTemplate>

<DataTemplate x:Key="dtThumbnail">
<Grid>
<Image Width="100" Height="75"
Source="{Binding '', Mode=OneWay, Path=Small}"
Stretch="Fill" Margin="5,5,5,5"/>
</Grid>
</DataTemplate>

<DataTemplate x:Key="dtLargePhoto">
<Grid VerticalAlignment="Top" HorizontalAlignment="Stretch" Height="Auto">
<Grid.RowDefinitions>



<RowDefinition Height="0.8*"/>
<RowDefinition Height="0.2*"/>
</Grid.RowDefinitions>
<Image HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Source="{Binding '', Mode=OneWay, Path=Large}"
Stretch="Uniform" Grid.Row="0"
Margin="0,0,0,0"
Visibility="{Binding Mode=OneWay, Path=ImageVisible}"/>
<CheckBox Content="{Binding '',Mode=OneWay, Path=FileName}"
Grid.Row="1" HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="Black"
Margin="0,0,0,0" FontSize="16" FontWeight="Bold"
x:Name="btnMeta" Checked="btnMeta_Checked"
Unchecked="btnMeta_Unchecked" />
<ProgressBar
Maximum="100" Minimum="100" Width="290" Foreground="Red" Height="30"
Value="{Binding Mode=OneWay, Path=PercentProgress}"
Visibility="{Binding Mode=OneWay, Path=ProgressVisible}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</DataTemplate>

<DataTemplate x:Key="dtPhotoMetaData">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="0.15*"/>
<RowDefinition Height="0.15*"/>
<RowDefinition Height="0.15*"/>
<RowDefinition Height="0.15*"/>
<RowDefinition Height="0.15*"/>
<RowDefinition Height="0.15*"/>
<RowDefinition Height="0.10*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.5*" />
<ColumnDefinition Width="0.5*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0"
Grid.ColumnSpan="2" Text="Edit Metadata"
HorizontalAlignment="Center"
VerticalAlignment="Center" Margin="3,3,3,3"/>
<TextBlock Grid.Row="1" Grid.Column="0"
Text="Name:" Margin="3,3,3,3" />



<TextBlock Grid.Row="2" Grid.Column="0"
Text="Description:" Margin="3,3,3,3" />
<TextBlock Grid.Row="3" Grid.Column="0"
Text="Location:" Margin="3,3,3,3" />
<TextBlock Grid.Row="4" Grid.Column="0"
Text="Rating:" Margin="3,3,3,3" />
<TextBlock Grid.Row="5" Grid.Column="0"
Text="Date Taken:" Margin="3,3,3,3" />
<TextBox Grid.Row="1" Grid.Column="1"
Text="{Binding Mode=TwoWay,Path=Info.Name}"
Width="275" Margin="3,3,3,3" />
<TextBox Grid.Row="2" Grid.Column="1"
Text="{Binding Mode=TwoWay,Path=Info.Description}"
Width="275" Margin="3,3,3,3" TextWrapping="Wrap"
AcceptsReturn="True" />
<TextBox Grid.Row="3" Grid.Column="1"
Text="{Binding Mode=TwoWay,Path=Info.Location}"
Width="275" Margin="3,3,3,3" TextWrapping="Wrap"
AcceptsReturn="True" />
<TextBox Grid.Row="4" Grid.Column="1"
Text="{Binding Mode=TwoWay,Path=Info.Rating}"
Width="275" Margin="3,3,3,3" />
<Controls:DatePicker Grid.Row="5" Grid.Column="1"
SelectedDate="{Binding Mode=TwoWay,Path=Info.DateTaken}"
Width="275" Margin="3,3,3,3"/>
<Button Content="Save Changes" x:Name="btnSaveMetaData"
Grid.Row="6" Grid.ColumnSpan="2" HorizontalAlignment="Center"
VerticalAlignment="Center" Height="30" Width="100"
Margin="10,10,10,10" Click="btnSaveMetaData_Click"/>
</Grid>
</DataTemplate>

<ControlTemplate x:Key="ctThumbnailListBoxItem" TargetType="ListBoxItem">
<Grid>
<vsm:VisualStateManager.VisualStateGroups>
<vsm:VisualStateGroup x:Name="CommonStates">
<vsm:VisualState x:Name="Normal">
<Storyboard/>
</vsm:VisualState>
<vsm:VisualState x:Name="MouseOver">
<Storyboard>
<ColorAnimationUsingKeyFrames
BeginTime="00:00:00"
Duration="00:00:00.0010000"
Storyboard.TargetName="brdrHover"



Storyboard.TargetProperty=
"(Border.BorderBrush).(SolidColorBrush.Color)">
<SplineColorKeyFrame
KeyTime="00:00:00" Value="#FF0748BD"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="SelectionStates">
<vsm:VisualState x:Name="Unselected"/>
<vsm:VisualState x:Name="Selected">
<Storyboard>
<ColorAnimationUsingKeyFrames
BeginTime="00:00:00"
Duration="00:00:00.0010000"
Storyboard.TargetName="brdrSelect"
Storyboard.TargetProperty=
"(Border.Background).(SolidColorBrush.Color)">
<SplineColorKeyFrame
KeyTime="00:00:00" Value="#FF0748BD"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
<vsm:VisualState x:Name="SelectedUnfocused"/>
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="FocusStates">
<vsm:VisualState x:Name="Unfocused"/>
<vsm:VisualState x:Name="Focused"/>
</vsm:VisualStateGroup>
</vsm:VisualStateManager.VisualStateGroups>
<Border HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
x:Name="brdrHover" BorderBrush="#FF000000"
BorderThickness="5" CornerRadius="3,3,3,3"
Margin="3,3,3,3" >
<Border CornerRadius="3,3,3,3" Padding="7,7,7,7"
Background="Transparent">
<Border x:Name="brdrSelect" Background="#FF9AE1F5"
CornerRadius="3,3,3,3" Padding="3,3,3,3" >
<ContentPresenter
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
HorizontalAlignment="Left"
/>
</Border>



</Border>
</Border>
</Grid>
</ControlTemplate>
<Style x:Key="styleThumbnailListBoxItem" TargetType="ListBoxItem">
<Setter Property="IsEnabled" Value="true" />
<Setter Property="Foreground" Value="#FF000000" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Top" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Background" Value="White" />
<Setter Property="Padding" Value="2,0,0,0" />
<Setter Property="Template" Value="{StaticResource ctThumbnailListBoxItem}"/>
</Style>
</UserControl.Resources>

<Grid Background="BurlyWood">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="150"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListBox HorizontalAlignment="Stretch"
Margin="5,5,5,5"
Width="Auto"
SelectionChanged="lbxThumbs_SelectionChanged"
ItemTemplate="{StaticResource dtThumbnail}"
x:Name="lbxThumbs"
ItemContainerStyle="{StaticResource styleThumbnailListBoxItem}"
Grid.ColumnSpan="2" Visibility="Collapsed">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
<StackPanel x:Name="visualThumbZipDownload" Margin="0,20,0,0">
<ProgressBar
Maximum="100" Minimum="0" Height="30" Foreground="Red"
Width="290" x:Name="pbarThumbZipDownload"
Visibility="Visible" HorizontalAlignment="Center"



VerticalAlignment="Center"/>
<Button x:Name="btnZipDownloadCancel"
Content="Cancel"
Click="btnZipDownloadCancel_Click"
HorizontalAlignment="Center" Width="125" />
</StackPanel>

<ContentControl x:Name="contentctlLargeImage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Grid.Row="1" Margin="8,8,8,8"
ContentTemplate="{StaticResource dtLargePhoto}"
Grid.RowSpan="1"/>
<ContentControl x:Name="contentctlImageInfo"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Grid.Row="1" Grid.Column="1"
Margin="8,0,8,0"
ContentTemplate="{StaticResource dtPhotoMetaData}"
Grid.RowSpan="1" Visibility="Collapsed"/>
<Grid HorizontalAlignment="Stretch" Margin="8,8,8,8"
VerticalAlignment="Stretch" Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.5*"/>
<ColumnDefinition Width="0.5*"/>
</Grid.ColumnDefinitions>
<Button HorizontalAlignment="Right"
VerticalAlignment="Stretch" Content="Previous"
Margin="8,0,8,0" Height="32.11" x:Name="btnPrevious"
Width="99.936"
Click="btnPrev_Click"/>
<Button Margin="8,0,8,0" VerticalAlignment="Stretch"
Content="Next" HorizontalAlignment="Left"
Height="31.11" x:Name="btnNext"
Grid.Column="1" Width="99.936"
Click="btnNext_Click"/>
<Button HorizontalAlignment="Left" Margin="0,0,0,0"
Width="100" Content="Upload" x:Name="btnUpload"
Click="btnUpload_Click"/>
</Grid>
</Grid>
</UserControl>


The main UI is made up of a ListBox named lbxThumbs and two ContentControls named contentctlLargeImage and contentctlImageInfo. A ProgressBar control is also used on MainPage, as well as Buttons for image navigation (btnPrevious and btnNext), a Button to cancel the thumbnail ZIP download (btnZipDownloadCancel), and a Button to upload a local image to the server (btnUpload).

You apply a custom Panel to the ListBox lbxThumbs to change its orientation to display the thumbnail items horizontally from left to right. You also apply a custom control template to each ListBoxItem, using the ItemContainerStyle property of the ListBox, to change the default look and feel of a ListBoxItem.

The dtLargePhoto data template is used to display a selected image and is made up of an Image control, a CheckBox control that can be used to toggle the visibility of the image's metadata, and a ProgressBar that displays the download progress of an image. The Image is bound to the Large property on the WrappedImage type. dtLargePhoto is applied to the ContentControl contentctlLargeImage in the main UI, using its ContentTemplate property.

The dtPhotoMetaData data template creates a data-entry form for image metadata. It has edit controls data-bound to properties in the PhotoMetadata data contract and is applied to the ContentControl contentctlImageInfo in the main UI again, with initial Visibility of the ContentControl set to Collapsed.

The dtThumbnail data template is applied to the ListBox lbxThumbnails through its ItemTemplate property. dtThumbnail also contains an Image control, bound to WrappedImage.Small.

Now, let's look at how the WebClient is used in this MainPage's codebehind to access resources and interact with web services. Listing 6 shows the codebehind for MainPage.

Listing 6. Codebehind for the PhotoClient application page in MainPage.xaml.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Net;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
using System.Windows.Resources;
using System.Xml.Linq;

namespace Recipe7_4.PhotoClient
{
public partial class Page : UserControl
{

private const string MetadataDownloadUri =
"http://localhost:9494/MetaData.svc";
private const string MetadataUploadUri =
"http://localhost:9494/MetaDataUpload.aspx";
private const string PhotoDownloadUri =
"http://localhost:9494/PhotoDownload.svc";
private const string PhotoUploadUri =
"http://localhost:9494/PhotoUpload.aspx";



ObservableCollection<WrappedImage> ImageSources =
new ObservableCollection<WrappedImage>();
WebClient wcThumbZip = new WebClient();
public Page()
{
InitializeComponent();
lbxThumbs.ItemsSource = ImageSources;
contentctlLargeImage.Content = new WrappedImage();
GetImageNames();
}

private void GetImageNames()
{
//create a WebClient
WebClient wcImageNames = new WebClient();
//attach a handler to the OpenReadCompleted event
wcImageNames.OpenReadCompleted +=
new OpenReadCompletedEventHandler(
delegate(object sender, OpenReadCompletedEventArgs e)
{
//initialize a JSON Serializer
DataContractJsonSerializer jsonSer =
new DataContractJsonSerializer(typeof(List<string>));
//deserialize the returned Stream to a List<string>
List<string> FileNames =
jsonSer.ReadObject(e.Result) as List<string>;
//start loading the thumbnails
LoadThumbNails(FileNames);
});
//Start reading the remote resource as a stream
wcImageNames.OpenReadAsync(
new Uri(string.Format("{0}/GetPhotoFileNames", MetadataDownloadUri)));

}

private void LoadThumbNails(List<string> ImageFileNames)
{
wcThumbZip.OpenReadCompleted +=
new OpenReadCompletedEventHandler(wcThumbZip_OpenReadCompleted);
wcThumbZip.DownloadProgressChanged +=
new DownloadProgressChangedEventHandler
(
delegate(object Sender, DownloadProgressChangedEventArgs e)
{



//set the progress bar value to the reported progress percentage
pbarThumbZipDownload.Value = e.ProgressPercentage;
}
);
//start reading the thumbnails zip file as a stream,
//pass in the ImageFileNames List<string> as user state
wcThumbZip.OpenReadAsync(
new Uri(
string.Format("{0}/GetThumbs", PhotoDownloadUri)), ImageFileNames);
}

void wcThumbZip_OpenReadCompleted(object sender,
OpenReadCompletedEventArgs e)
{
//if operation was cancelled, return.
if (e.Cancelled) return;
//grab the passed in user state from
//e.UserState, and cast it appropriately
List<string> FileNames = e.UserState as List<string>;
//create a StreamResourceInfo wrapping the returned stream,
//with content type set to .PNG
StreamResourceInfo resInfo = new StreamResourceInfo(e.Result, "image/png");
//for each file name
for (int i = 0; i < FileNames.Count; i++)
{
//create and initialize a WrappedImage instance
WrappedImage wi =
new WrappedImage
{
Small = new BitmapImage(),
Large = null,
FileName = FileNames[i] + ".jpg",
ThumbName = FileNames[i] + ".png"
};
try
{
//Read the thumbnail image from the returned stream (the zip file)
Stream ThumbStream = Application.GetResourceStream(
resInfo, new Uri(wi.ThumbName, UriKind.Relative)).Stream;
//and save it in the WrappedImage instance
wi.Small.SetSource(ThumbStream);
//and bind it to the thumbnail listbox
ImageSources.Add(wi);
}
catch


}
}
//hide the progress bar and show the ListBox
visualThumbZipDownload.Visibility = Visibility.Collapsed;
lbxThumbs.Visibility = Visibility.Visible;
}
private void btnZipDownloadCancel_Click(object sender, RoutedEventArgs e)
{
//if downloading thumbnail zip , issue an async request to cancel
if (wcThumbZip != null && wcThumbZip.IsBusy)
wcThumbZip.CancelAsync();
}

//thumbnail selection changed
private void lbxThumbs_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
//get the WrappedImage bound to the selected item
WrappedImage wi = (e.AddedItems[0] as WrappedImage);
//bind it to the large image display, as well to the metadata display
contentctlLargeImage.Content = wi;
contentctlImageInfo.Content = wi;
//if the large image has not been downloaded
if (wi.Large == null)
{
//display the progress bar and hid the large image control
wi.ProgressVisible = Visibility.Visible;
wi.ImageVisible = Visibility.Collapsed;
//initialize the BitmapImage for the large image
wi.Large = new BitmapImage();
//new web client
WebClient wcLargePhoto = new WebClient();
//progress change handler
wcLargePhoto.DownloadProgressChanged +=
new DownloadProgressChangedEventHandler(
delegate(object Sender, DownloadProgressChangedEventArgs e1)
{
//update value bound to progress bar
wi.PercentProgress = e1.ProgressPercentage;
});
//completion handler
wcLargePhoto.DownloadStringCompleted +=
new DownloadStringCompletedEventHandler(
wcLargePhoto_DownloadStringCompleted);

//download image bytes as a string, pass
//in WrappedImage instance as user supplied state
wcLargePhoto.DownloadStringAsync(
new Uri(string.Format("{0}/Photos?Name={1}",
PhotoDownloadUri, wi.FileName)), wi);
}
}
//large image download completed
void wcLargePhoto_DownloadStringCompleted(object sender,
DownloadStringCompletedEventArgs e)
{
//get the WrappedImage instance from user supplied state
WrappedImage wi = (e.UserState as WrappedImage);
//parse XML formatted response string into an XDocument
XDocument xDoc = XDocument.Parse(e.Result);
//grab the root, and decode the default base64
//representation into the image bytes
byte[] Buff = Convert.FromBase64String((string)xDoc.Root);
//wrap in a memory stream, and
MemoryStream ms = new MemoryStream(Buff);
wi.Large.SetSource(ms);
wi.ProgressVisible = Visibility.Collapsed;
wi.ImageVisible = Visibility.Visible;
GetPhotoMetadata(wi);
}

private void btnPrev_Click(object sender, RoutedEventArgs e)
{
if (lbxThumbs.SelectedIndex == 0) return;
lbxThumbs.SelectedIndex = lbxThumbs.SelectedIndex − 1;
}

private void btnNext_Click(object sender, RoutedEventArgs e)
{
if (lbxThumbs.SelectedIndex == lbxThumbs.Items.Count − 1) return;
lbxThumbs.SelectedIndex = lbxThumbs.SelectedIndex + 1;
}

private void btnMeta_Checked(object sender, RoutedEventArgs e)
{
contentctlImageInfo.Visibility = Visibility.Visible;
}

private void btnMeta_Unchecked(object sender, RoutedEventArgs e)
{


contentctlImageInfo.Visibility = Visibility.Collapsed;
}

private void GetPhotoMetadata(WrappedImage wi)
{

WebClient wcMetadataDownload = new WebClient();
wcMetadataDownload.DownloadStringCompleted +=
new DownloadStringCompletedEventHandler(
delegate(object sender, DownloadStringCompletedEventArgs e)
{
DataContractJsonSerializer JsonSer =
new DataContractJsonSerializer(typeof(PhotoMetaData));
//decode UTF8 string to byte[], wrap in a memory string and
//deserialize to PhotoMetadata using DatacontractJsonSerializer
PhotoMetaData pmd = JsonSer.ReadObject(
new MemoryStream(new UTF8Encoding().GetBytes(e.Result)))
as PhotoMetaData;
//data bind
(e.UserState as WrappedImage).Info = pmd;
});
wcMetadataDownload.DownloadStringAsync(
new Uri(string.Format("{0}/PhotoMetadata?Id={1}",
MetadataDownloadUri,
wi.FileName)), wi);
}

private void btnSaveMetaData_Click(object sender, RoutedEventArgs e)
{
SetPhotoMetadata(contentctlImageInfo.Content as WrappedImage);
}
//upload metadata
private void SetPhotoMetadata(WrappedImage wi)
{
//new WebClient
WebClient wcMetadataUpload = new WebClient();
//serialize the metadata as JSON
DataContractJsonSerializer JsonSer =
new DataContractJsonSerializer(typeof(PhotoMetaData));
MemoryStream ms = new MemoryStream();
JsonSer.WriteObject(ms, wi.Info);
//convert serialized form to a string
string SerOutput = new UTF8Encoding().
GetString(ms.GetBuffer(), 0, (int)ms.Length);
ms.Close();


//upload string
wcMetadataUpload.UploadStringAsync(
new Uri(MetadataUploadUri), "POST",
SerOutput);
}


//upload local image file
private void btnUpload_Click(object sender, RoutedEventArgs e)
{
//open a file dialog and allow the user to select local image files
OpenFileDialog ofd = new OpenFileDialog();
ofd.Filter = "JPEG Images|*.jpg;*.jpeg";
ofd.Multiselect = true;
if (ofd.ShowDialog() == false) return;
//for each selected file
foreach (FileInfo fdfi in ofd.Files)
{
//new web client
WebClient wcPhotoUpload = new WebClient();
//content type
//wcPhotoUpload.Headers["Content-Type"] = "image/jpeg";
//name of the file as a custom property in header
wcPhotoUpload.Headers["Image-Name"] = fdfi.Name;
wcPhotoUpload.OpenWriteCompleted +=
new OpenWriteCompletedEventHandler(wcPhotoUpload_OpenWriteCompleted);
//upload image file - pass in the image file stream as user supplied state
wcPhotoUpload.OpenWriteAsync(new Uri(PhotoUploadUri),
"POST", fdfi.OpenRead());
}
}

void wcPhotoUpload_OpenWriteCompleted(object sender,
OpenWriteCompletedEventArgs e)
{
//get the image file stream from the user supplied state
Stream imageStream = e.UserState as Stream;
//write the image file out to the upload stream available in e.Result
int ChunkSize = 1024 * 1024;
int ReadCount = 0;
byte[] Buff = new byte[ChunkSize];
do
{
ReadCount = imageStream.Read(Buff, 0, ChunkSize);
e.Result.Write(Buff, 0, ReadCount);


} while (ReadCount == ChunkSize);
//close upload stream and return - framework will upload in the background
e.Result.Close();
}
}
}


The GetImageNames() method uses WebClient.OpenReadAsync() to acquire a list of names for all the image files available to you for download. In the operation contract for IMetaData.GetPhotoFileNames() in Listing 1, notice that the response format is specified as JSON. In the WebClient.OpenReadCompleted event handler (implemented using the C# anonymous delegate feature), you use the DataContractJsonSerializer to deserialize content from the returned stream into a List<string> of the file names. You then call the LoadThumbnails() method, passing in the list of file names.

The LoadThumbnails() method uses the WebClient.OpenReadAsync() method again to start downloading the thumbnail ZIP file. In the WebClient.DownloadProgressChanged event handler, the ProgressBar control pbarThumbZipDownload is updated with the percentage of progress. In case of a long download, a Cancel button is provided. You handle the cancellation in btnZipDownloadCancel_Click(), where you check to see if the WebClient is currently downloading using the IsBusy property, and if so, issue a cancellation request. The UI for thumbnail ZIP download and cancellation is shown in Figure 2.

Figure 2. Thumbnail ZIP download

The OpenReadCompleted handler wcThumbZip_OpenReadCompleted() first checks to see if the operation was canceled. If not, the file name list is retrieved from the user state, and each thumbnail is retrieved from the ZIP file using the Application.GetResourceStream() method. This method can read individual streams compressed inside a ZIP, as long as the correct content type (in this case, image/png) is provided using the StreamResourceInfo type parameter. The returned stream from GetResourceStream() is the thumbnail file, which is data-bound to the UI via a new instance of a WrappedImage. You create the WrappedImage, initialize its Small property to the thumbnail image, set its FileName and ThumbName properties, and then add it to the ImageSources collection. The ImageSources collection was already bound to lbxThumbs as its ItemsSource in the constructor of the page.

Now, let's look at downloading the full image and its metadata. In the SelectionChanged handler lbxThumbs_SelectionChanged() for the thumbnails ListBox, you acquire the WrappedImage instance bound to the current thumbnail and bind it to the ContentControl contentctlLargeImage as well. You then determine whether the image corresponding to that thumbnail has been downloaded already by checking the WrappedImage.Large property for null. If it is null, you use the DownloadStringAsync() method to download the image. The operation contract of the IPhotoDownload.GetPhoto() in Listing 1 shows you that the image is being returned from the service as an array of bytes, but the default WCF DataContractSerializer knows how to serialize the byte[] to a Base64-encoded string. The message returned from GetPhoto() in the completion handler wcLargePhoto_DownloadStringCompleted() is an XML fragment, containing only one element: the Base64-encoded string representing the image. You access the result as an XDocument instance, parsing it using the XDocument.Parse() method. You then decode the root of this XDocument instance back to an array of bytes. You wrap it into a temporary memory stream, set it as the source for the BitmapImage bound to the large image control, and proceed to fetch the metadata.

The PhotoMetadata is returned from the service formatted as JSON. The GetPhotoMetadata() method also uses DownloadStringAsync() to acquire the metadata, decodes the downloaded string from its UTF8 string form to the constituent byte array, deserializes the byte array using the DataContractJasonSerializer, and then binds the resulting PhotoMetadata instance to the metadata UI through the WrappedImage.Info property.

In the SetPhotoMetadata() method, the PhotoMetadata instance is serialized to JSON and then encoded to a UTF8 string, which is then uploaded using the UploadStringAsync() method. Note that the upload uses the MetadataUpload.aspx page as the endpoint. This code sample does not handle the upload-completion event, but you can do so to check for any upload errors.

The last piece of this solution is the image-upload logic. In the click handler btnUpload_Click() for the Upload button, you use the OpenFileDialog to allow the user to select one or more local image files. Each image file is then uploaded using OpenWriteAsync(). Note that the Content-Type HTTP header is set to the image/jpeg MIME type to ensure proper encoding. Also note the use of the custom header property Image-Name to upload the name of the image file. As shown in Listing 3, this is extracted and used in the codebehind of the PhotoUpload.aspx page to name the image file on the server, after it has been uploaded.

As mentioned earlier, OpenWriteAsync() immediately calls the completion handler wcPhotoUpload_OpenWriteCompleted(), where you write the image file to the upload stream made available through the OpenWriteCompletedEventArgs.Result property. When the stream is closed and the handler returns, the framework uploads the file asynchronously.

NOTE

You may have noticed the absence of any upload-progress notification handlers. Silverlight does not supply any upload-progress notifications, although future versions may.

Other  
  •  Silverlight Recipes : Networking and Web Service Integration - Using JSON Serialization over HTTP
  •  Microsoft XNA Game Studio 3.0 : Displaying Images - Using Resources in a Game (part 4) - Filling the Screen
  •  Microsoft XNA Game Studio 3.0 : Displaying Images - Using Resources in a Game (part 3) - Sprite Drawing with SpriteBatch
  •  Microsoft XNA Game Studio 3.0 : Displaying Images - Using Resources in a Game (part 2) - Positioning Your Game Sprite on the Screen
  •  Microsoft XNA Game Studio 3.0 : Displaying Images - Using Resources in a Game (part 1) - Loading XNA Textures
  •  iPhone 3D Programming : Holodeck Sample (part 5) - Overlaying with a Live Camera Image
  •  iPhone 3D Programming : Holodeck Sample (part 4) - Replacing Buttons with Orientation Sensors
  •  iPhone 3D Programming : Holodeck Sample (part 3) - Handling the Heads-Up Display
  •  iPhone 3D Programming : Holodeck Sample (part 2) - Rendering the Dome, Clouds, and Text
  •  iPhone 3D Programming : Holodeck Sample (part 1) - Application Skeleton
  •  Building LOB Applications : Printing in a Silverlight LOB Application
  •  Building LOB Applications : Data Validation through Data Annotation
  •  Building LOB Applications : Implementing CRUD Operations in RIA Services
  •  Microsoft XNA Game Studio 3.0 : Displaying Images - Resources and Content (part 2) - Adding Resources to a Project
  •  Microsoft XNA Game Studio 3.0 : Displaying Images - Resources and Content (part 1)
  •  iPhone 3D Programming : Blending and Augmented Reality - Rendering Anti-Aliased Lines with Textures
  •  Programming with DirectX : Game Math - Bounding Geometry (part 2) - Bounding Spheres & Bounding Hierarchies
  •  Programming with DirectX : Game Math - Bounding Geometry (part 1) - Bounding Boxes
  •  Programming with DirectX : Game Math - Matrices
  •  iPhone 3D Programming : Anti-Aliasing Tricks with Offscreen FBOs (part 2) - Jittering
  •  
    Most View
    Golden Media Spark One - Plenty To Offer Out Of The Box (Part 1)
    Introducing UEFI BIOS (Part 2)
    How To Automate Your Web With ifttt (Part 1)
    Corsair Carbide 200r - Joy To Build
    Panasonic Lumix DMC-SZ9 - Lots Of Smart Features In A Very Small Camera
    HTC One - A Huge Leap For Android Smartphones
    Windows Server 2003 : Advanced Backup and Restore (part 1) - Backup Options, The Ntbackup Command
    Linux vs Windows 8 (Part 5)
    Gigabyte Osmium Aivia Mechanical Keyboard
    The Complete Guide To Photography On Your Mac! (Part 2)
    Top 10
    Does Microsoft Have An Image Problem? (Part 2)
    Does Microsoft Have An Image Problem? (Part 1)
    Time For A Bigger iPhone?
    99 Mac Secrets (Part 5) - Top ten third-party apps
    99 Mac Secrets (Part 4) - iMovie secrets, GarageBand secrets, iWork secrets
    99 Mac Secrets (Part 3) : Safari secrets, Mail secrets, Safari shortcuts, Mail shortcuts, iPhoto secrets
    99 Mac Secrets (Part 2) : Customizing, Best menu bar add-ons, Quick Look secrets
    99 Mac Secrets (Part 1) : General OS X tips, Security tips, System shortcuts
    iMovie Trailers And Audio Premastered
    PowerTraveller Powerchimp 4A Battery Charger