In This Chapter
So far, our Silverlight applications have run isolated from the server they originated from. We loaded images and videos, but after that the application didn’t communicate with the server anymore. In this chapter, we change this and demonstrate three different ways to place calls to the web server.
We talked about XML as early as Chapter 2, “Understanding XAML,” as a good and modern way to store information. In this chapter, we will apply this to our Thumbnails application. After taking the media files themselves out of the application, we finally do the same with the media information.
The first step in creating and loading an XML file is to export the media information from the Page.xaml resources into an external file. It will be easier to modify this file, to add or remove media, without having to recompile the application. Follow the steps:
1. Open the Thumbnails application in Visual Studio. In Thumbnails.Web, right click on the ClientBin folder in the Solution Explorer and select Add New Item.
2. Select an XML file and name it media.xml.
3. Set the content shown in Listing 22.1.
<?xml version=″1.0″ encoding=″utf-8″ ?>
<medias>
<media type=″Movie″ name=″mov1.wmv″
description=″Nightly show at Singapore Zoo″/>
<media type=″Image″ name=″pic1.png″
description=″The Matterhorn seen from Zermatt″/>
<media type=″Image″ name=″pic2.jpg″
description=″The Matterhorn″/>
<media type=″Image″ name=″pic3.jpg″
description=″Mountains seen from Klosters″/0>
</medias>
4. In the Thumbnails project, open Page.xaml and empty the MediaDataSource
:
<data:MediaExCollection x:Key=″MediaDataSource″
d:IsDataSource=″True″ />
5. In Page.xaml.cs, to avoid errors, in the Page
constructor, delete the code used to initialize the TextBlocks Foreground
brush. Delete the lines starting at MediaExCollection mediaCollection
and ending at TitleTextBlock2.Foreground = brush;
.
You can build the application to make sure that everything is OK. You can even run it, but of course the viewer will be empty, since we removed the content of the data source.
We will now add a startup screen to the application. It will simply cover the whole screen and be half transparent. We will also use it to display information to the user. If you want, you can of course place additional features on this screen, such as an animation, a logo, and so on. We will make sure that the startup screen is displayed before we load the XML file and the thumbnails. This way, if the network is slow, the user gets information, instead of staring at an empty screen.
In Page.xaml, add a “cache” at the end of the main Grid
LayoutRoot (but still inside it). Since the “cache” appears after every other element, it will be on top of them and intercept every mouse click. By making it half transparent (well, 70% opaque, actually), we let the user get a preview of what to expect. Of course, the cache must cover everything, so we use the Grid.RowSpan
and Grid.ColumnSpan
attributes as shown in Listing 22.2. Add this markup at the end of the file, but still in the LayoutRoot:
<StackPanel x:Name=″Cache″ Background=″#CCFFFFFF″
Grid.RowSpan=″2″ Grid.ColumnSpan=″2″>
<TextBlock Text=″Please wait...″ TextWrapping=″Wrap″
x:Name=″StatusTextBlock″ FontSize=″36″
RenderTransformOrigin=″0.5,0.5″
Margin=″25,80,25,0″ HorizontalAlignment=″Left″>
<TextBlock.RenderTransform>
<RotateTransform Angle=″-10″ />
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock TextWrapping=″Wrap″ x:Name=″ErrorTextBlock″
Margin=″25,70,25,0″ HorizontalAlignment=″Stretch″ />
</StackPanel>
We don’t want to simply hide the “cache,” so we draft a simple fade out animation. You can do this in Blend, or simply type the XAML markup in Listing 22.3 at the end of the UserControl.Resources
:
<Storyboard x:Name=″CacheFadeOutStoryboard″>
<DoubleAnimationUsingKeyFrames BeginTime=″00:00:00″
Storyboard.TargetName=″Cache″
Storyboard.TargetProperty=″(UIElement.Opacity)″>
<SplineDoubleKeyFrame KeyTime=″00:00:00.5000000″ Value=″0″ />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
We encapsulate the loading and parsing of the XML media file in a separate object. This way, it is easier to modify it if the structure of the XML file changes, if we decide to create a web service to deliver this information, for example.
1. Inside the Data folder in the Thumbnails project, add a new class and name it MediaInformationFile.cs.
2. Before the MediaInformationFile
class itself, add a new class deriving from EventArgs
(Listing 22.4). We use this class to communicate the results to the outside world.
public class MediaFileLoadedEventArgs : EventArgs
{
public IEnumerable<MediaEx> Collection
{
get; set;
}
public Exception Error
{
get; set;
}
}
3. In the beginning of the MediaInformationFile
class, add two constants to store the name of the XML media file and the path to the media files:
public const string MEDIA_FILE_NAME = ″media.xml″;
public const string MEDIA_PATH = ″MediaFiles″;
4. Declare a new event inside the class. We use that event to inform any subscriber that the information contained in the XML file is loaded and ready for use. Together with this event, let’s declare a companion method like we did a couple of times before, as shown in Listing 22.5:
public event EventHandler Loaded;
private void OnMediaFileLoaded(IEnumerable<MediaEx> xmlMedias,
Exception lastError)
{
if (Loaded != null)
{
MediaFileLoadedEventArgs args = new MediaFileLoadedEventArgs();
args.Collection = xmlMedias;
args.Error = lastError;
Loaded(this, args);
}
}
If someone subscribed to the event, the method creates a new MediaFileLoadedEventArgs
and stores the collection of loaded MediaEx
instances (as read from the XML file).
In case something bad happened, we also pass the last error to the event subscriber. This error is of type Exception
. We talk more about them in Chapter 23, “Placing Cross-Domain Requests and Handling Exceptions.”
Instead of passing an ObservableCollection
(such as an instance of the MediaExCollection
that we created before), we use an IEnumerable<MediaEx>
. This gives us a great freedom: The only thing that the subscriber knows for sure is that they can enumerate (for example, with a foreach
loop) through the content of the collection. The reason we prefer to use an IEnumerable<MediaEx>
is that it avoids an additional conversion as we see in a moment. Besides, since ObservableCollection
(and thus MediaExCollection
) implements IEnumerable
, too, we can change this later without having to modify the MediaFileLoadedEventArgs
at all.
Let’s add some flesh to the MediaInformationFile
class.
1. Add the DLL System.Net to the application. This DLL contains the WebClient
class that we want to use. Because it is not part of the standard distribution of Silverlight, we must add it explicitly. Right click on the Thumbnails project and select Add Reference. Then, choose the System.Net DLL from the .NET tab.
2. Create a public method in the MediaInformationFile
class as shown in Listing 22.6:
1 public void LoadMediaFile()
2 {
3 WebClient client = new WebClient();
4 client.DownloadStringCompleted
5 += new DownloadStringCompletedEventHandler(
6 client_DownloadStringCompleted);
7 client.DownloadStringAsync(new Uri(MEDIA_FILE_NAME, UriKind.Relative));
8 }
The WebClient
class is one of the two main classes you will use for web communication. This is the simplest way to send a request and get a response.
This class has various methods. Since we need to download XML content, and we know that this is text, we use the method DownloadStringAsync
, the easiest way to get text content from the web server.
The suffix Async
is short for asynchronous. In Silverlight, all the web communication occurs asynchronously. Once the request is sent, the Silverlight application continues to run without waiting for the response. When the response arrives, an event is raised by the framework.
In the full version of .NET, there is a way to send synchronous requests to a web server, and to block the application as long as the response doesn’t arrive, but synchronous communication is not included in Silverlight.
As we said previously, we need to handle an event when the response from the web server reaches the Silverlight application. The event corresponding to the DownloadStringAsync
method is the DownloadStringCompleted
event. So we need to register a DownloadStringCompletedEventHandler
event handler (lines 4 to 6).
Finally, we call the method itself, providing a relative URI to the media files (line 7).
This method presupposes that the media will be placed into the MediaFiles folder and the XML information file in ClientBin. These types of constraints are exactly what user documentation is for: Don’t forget to mention this fact to your users, to make sure that they comply.
After the request is sent, the user can continue to work, but in this case it’s not very interesting, since this is an initial download, and the user is just waiting. There is a way to provide information about the progress of the download, and we talk about this later in the chapter. For now, since the XML file is usually rather small, we just wait. Once the request arrives, the event is raised, and we handle it with Listing 22.7. Add this code in the MediaInformationFile
class.
1 void client_DownloadStringCompleted(object sender,
2 DownloadStringCompletedEventArgs e)
3 {
4 // Prepare data
5 IEnumerable<MediaEx> output = null;
6 Exception lastError = null;
7 try
8 {
9 // Get media collection here
10 }
11 catch (Exception ex)
12 {
13 // Failure, inform user
14 if (ex.InnerException != null
15 && ex.InnerException is WebException)
16 {
17 lastError = ex.InnerException;
18 }
19 else
20 {
21 lastError = ex;
22 }
23 output = null;
24 }
25
26 OnMediaFileLoaded(output, lastError);
27 }
On line 5, we declare an IEnumerable<MediaEx>
. We know that an interface cannot be instantiated (because it is abstract), but the framework returns a class implementing this interface when we parse the XML file later. In fact we don’t really care what exact class we get from the framework. The only thing that we are interested in is being able to enumerate all the items.
On line 6 we create a variable for an error that could occur when loading the XML file.
Because we don’t know what other errors can occur, we catch just any Exception
type (line 11). In fact, it is not very good practice. As we see in Chapter 23, it is better to differentiate according to the Exception
’s type.
Finally, we raise the Loaded
event on line 26, to notify anyone interested that the collection is ready (or that an error occurred, in which case the Error
property is set, and the Collection
property is null).
At this point, you can build the application after adding a couple of using
directives. Something big is missing though: Did you notice that we never actually read the media information from the response? This part deserves a section of its own.
On line 9 in the Listing 22.7 example, we must Get media collection here
. To perform this task, the easiest way is to use LINQ.
LINQ stands for Language Integrated Query and certainly deserves a book just for itself. Let’s summarize aggressively, and say that LINQ is a query framework integrated in the .NET framework (and as a subset in Silverlight). The whole version of LINQ (available in .NET) can perform queries against a number of data sources, including databases, object collections (for example, arrays, lists, and so on). In this section, we use LINQ to perform queries against XML data stores. In Chapter 23, we will also see that you can use LINQ to perform queries from JSON-formatted data sources.
Before LINQ was created, the usual way to perform queries was by using another specialized and complex programming language called SQL. However, SQL has many disadvantages: It’s text-based (so it’s easy to make mistakes); it is only available to query against databases; it comes in different flavors, depending on the “brand” of database you work with; and it is not integrated but is external to .NET.
Although LINQ is comfortable to use (after you overcome the small learning curve), it is not the fastest method to parse XML data. For very big documents, to avoid waiting for the whole structure to be loaded in memory, you should instead use the XmlReader
class. This is outside the scope of this book, however. Check the SDK documentation for details.
We lack the time to go into much detail about LINQ, even the reduced version available in Silverlight. In this section, we show how to load a collection of MediaEx
items with the following steps:
1. LINQ is also not part of the standard Silverlight distribution, so we need to add the DLL to the application (just like the System.Net DLL before). Add a reference to the System.Xml.Linq DLL.
2. Add a private attribute on top of the class MediaInformationFile
:
private XDocument _document;
3. Additionally to the namespace System.Xml.Linq
that you can add for the XDocument
attribute we added in step 2, you need to add another using
statement. This cannot be added automatically (pressing Shift+Alt+F10 doesn’t work here), so simply type the following in the using
section on top of MediaInformationFile.cs:
using System.Linq;
4. Replace line 9 of Listing 22.7 (// Get media collection here
) with the code shown in Listing 22.8. Then, you can build the application.
1 StringReader reader = new StringReader(e.Result);
2 _document = XDocument.Load(reader);
3 output = from xmlMediaInfo
4 in _document.Descendants(″media″)
5 select new MediaEx
6 {
7 MediaPath = MEDIA_PATH,
8 MediaName = (string) xmlMediaInfo.Attribute(″name″),
9 Type = (MediaType) Enum.Parse(typeof(MediaType),
10 (string) xmlMediaInfo.Attribute(″type″), true),
11 Description = (string) xmlMediaInfo.Attribute(″description″)
12 };
For once, let’s start with line 2: The flavor of LINQ that we use here (called LINQ to XML) uses a class named XDocument
. Once an XML file is loaded into an instance of this class, we can query against it. Here, we use the Load
method, which takes a reader as parameter.
Depending on which kind of input you deal with, you can use various readers. We already know the StreamReader
is suited to load streams (and we use it again soon). There are many other types of readers however.
On line 1, we use the property e.Result
to build the reader. For the DownloadStringAsync
method, e.Result
is of type string
. This is really simple: We asked for a string, and we got one. The logical choice for a reader is a StringReader, created on line 1.
Line 3 uses the from
keyword to declare a variable named xmlMediaInfo
. The next line specifies where we get this variable (this is the input taken from the XML file). Note that this is only the beginning of the query, which ends on line 12 with a semicolon.
Line 4 specifies the collection in which we find the items to query. Here, this collection is returned by the XDocument
: We get all the Descendants
(children) of the main root that use the tag media
. If you take a look back at Listing 22.1, you’ll see that a media
element describes one media item.
Line 5 uses the select
keyword. Like the name suggests, this keyword selects items inside the input collection. We do not set a condition, so all items will be loaded. Also on line 5 we inform the framework that we will create instances of the MediaEx
class according to the XML data. Because of this statement, the LINQ query returns an IEnumerable<MediaEx>
that we store in the output
variable.
We need to specify how to create the instances of MediaEx
that will be stored in the collection. This is the purpose of lines 6 to 12. In these lines, the properties of the MediaEx
instance are set one by one.
Line 7 sets the MediaPath
property to the constant we declared before.
Line 8 gets the name
attribute from the XML element and assigns it to the MediaName
property.
Lines 9 and 10 also get an attribute (type
) from the XML element. XML attributes are strings
and must be converted before we can use them in Silverlight (unless, of course, they really are strings
, like the MediaName
property). Here, the Type
property is a MediaType
. This enumeration is declared in the file Media.cs. To convert a string
into an enum
, we need to parse it using the static method Enum.Parse
described in the next section, “Parsing Enums.”
Finally, the Description
is also a string
, so it is read directly from the XML element.
The method Enum.Parse
is complex, so let’s review it:
The first parameter of this method is the type of enumeration we want to get, for example typeof(MediaType)
.
The second parameter is the string we want to parse. This could be the string ″Image″
, for example, or the value of an XML attribute.
The third parameter is a bool
. If it’s true
, the parser ignores the casing of the string
. In that case the strings ″Image″
, ″Image″
, and ″ImAgE″
are interpreted the same. If it’s false
, wrongly cased strings are not parsed, and an error is thrown.
The Parse
method returns an object
that needs to be casted to the desired enumeration type.
Once again the entire infrastructure is in place now and needs to be triggered. This is the role of the Page
class. We send the request after the whole page has been loaded. The right moment for this is when the Page.Loaded
event is raised.
1. In Page.xaml.cs, add a private attribute to hold the instance of MediaInformationFile
that loads the XML file.
private MediaInformationFile _mediaFile;
2. Add an event handler for the Page.Loaded
event. You should place this line at the end of the Page
constructor.
this.Loaded += new RoutedEventHandler(Page_Loaded);
3. Implement the event handler (Listing 22.9):
void Page_Loaded(object sender, RoutedEventArgs e)
{
_mediaFile = new MediaInformationFile();
_mediaFile.Loaded += new EventHandler(_mediaFile_Loaded);
_mediaFile.LoadMediaFile();
}
Because the whole request/response/parsing business is encapsulated into the MediaInformationFile
class, we simply need to create an instance of this class, subscribe to its Loaded
event, and then call the LoadMediaFile
method, using the path of the media files as a parameter.
Like we mentioned, the file gets loaded asynchronously. Once it is fully loaded, and the collection of MediaEx
instances has been parsed, we are notified by an event raised by the MediaInformationFile
class, and we handle it with the following steps.
1. We registered for the MediaInformationFile.Loaded event earlier, so now we implement the _mediaFile_Loaded
event handler in the Page
class. The code in Listing 22.10 shows the empty event handler.
void _mediaFile_Loaded(object sender, EventArgs e)
{
}
2. Inside the event handler, cast the generic EventArgs
instance to MediaFileLoadedEventArgs
. Note that we could also have declared a specific delegate type for the MediaInformationFile.Loaded
event like we did in Chapter 10, “Progressing with .NET,” in the section titled “Raising Events and Using Delegates.”
MediaFileLoadedEventArgs args = e as MediaFileLoadedEventArgs;
3. In case there was an error, the args.Collection
will be null. Additionally, the args.Error
might contain additional information about the cause of the error. If that’s the case, we display this to the user and we exit. Note that the cache will never be removed. At this point, the user should contact her support department. The code in Listing 22.11 comes after what we added in step 2.
if (args.Collection == null)
{
StatusTextBlock.Text = ″There was an error″;
if (args.Error != null)
{
ErrorTextBlock.Text = args.Error.Message
+ Environment.NewLine + args.Error.StackTrace;
}
return;
}
4. Then get the data source collection out of the resources. Because this is an ObservableCollection
, when we add new items to it (those taken from the XML file), the Silverlight framework automatically looks for the DataTemplate
specified in the ItemsControl
and creates a visual representation.
MediaExCollection mediaCollection
= Resources[″MediaDataSource″] as MediaExCollection;
5. Since the result of the XML parsing is an IEnumerable<MediaEx>
, we can enumerate it and add the items to our own collection. We also use the occasion to look for content appropriate to use as a brush for the title TextBlocks
on the page. Add the code in Listing 22.12 in _mediaFile_Loaded
.
bool foundBrush = false;
foreach (MediaEx mediaInfo in args.Collection)
{
mediaCollection.Add(mediaInfo);
if (!foundBrush
&& mediaInfo.Type == MediaInfo.MediaType.Image)
{
ImageBrush brush = new ImageBrush();
brush.ImageSource = mediaInfo.ImageSource;
TitleTextBlock1.Foreground = brush;
TitleTextBlock2.Foreground = brush;
foundBrush = true;
}
}
Adding the items to the collection is straightforward since LINQ created them already of the right type. No need to cast.
If the item represents an image, and if this is the first image that we find, we use it to create an ImageBrush
for the TextBlocks
.
6. If everything went well, the cache must disappear, so that the user can start clicking on the thumbnails. We created an animation for this, so let’s retrieve it and use it with Listing 22.13, to be added in _mediaFile_Loaded
.
Storyboard cacheFadeOut
= Resources[″CacheFadeOutStoryboard″] as Storyboard;
cacheFadeOut.Completed
+= new EventHandler(cacheFadeOut_Completed);
cacheFadeOut.Begin();
7. Because the cache is in front of every other element, simply setting its Opacity
to 0% still prevents us from using the application. This is why we set a handler for the storyboard’s Completed
event, as shown in Listing 22.14:
void cacheFadeOut_Completed(object sender, EventArgs e)
{
Cache.Visibility = Visibility.Collapsed;
}
As soon as the cache is completely transparent, we collapse it so that it doesn’t get in the way anymore.
Run the application now. The media files are loaded according to the information contained in the XML media file. To verify this, exit the web browser application and modify the XML file, for example, change a description, add or remove an item, or change the order of the thumbnails. Then load the page again. You should now see the thumbnails according to the new XML file, without having to recompile the application.
Remember what we said in the very beginning of Chapter 21: The Silverlight application is cached by the web browser (and that includes the XML file that it downloads). Simply refreshing the web page might not be enough. To load the file anew, you need to empty the cache first. Check Chapter 21 for details.
Our application sends separate requests for each of the media files. We simply set the URI of a MediaElement
, an Image
, or an ImageBrush
, and the web browser sends the corresponding HTTP request. Depending on your configuration, this might not be the best way to do things.
We show another way to handle this: Pack all the media files into a Zip file and download this big file with the WebClient
class.
It is difficult to say which method is the best.
Handling multiple smaller responses might be more suited for slow connections, while one big download is better for a faster line.
Loading the media files one by one displays them faster to the user, because as soon as one picture or video is down, it appears in the ThumbnailsViewer control.
The big Zip file, once loaded, contains all the media needed to run the application. The application starts slower but is more responsive then, because the media is already available on the computer when the cache disappears.
If you load all the media files in a Zip file, your application has full control over the content; it can modify the files before displaying them, for example, resizing them, filtering them, and so on.
Choosing one or the other requires a fine analysis of the conditions in which your application will run, of the requirements, and so on.
Our first task is to create a Zip file instead of a folder. Use your preferred compression application to pack the whole MediaFiles folder into a Zip file. The Zip file should remain in the ClientBin folder.
You must make sure that the elements inside the Zip file are under the path MediaFiles and not in the root of the Zip file. With WinZip, for example, you can right-click on the ClientBin/MediaFiles folder and choose the Windows context menu WinZip, Add to MediaFiles.zip.
MediaEx
Class to Store the StreamThe MediaEx
class gets two new properties. Now that we have the whole media file in the Zip file, we extract it and save it in memory.
In MediaEx
(in the Data folder of the Thumbnails project), delete the existing properties named ImageSource
and MovieUri
and instead add the properties shown in Listing 22.15:
public Stream MediaSource
{
get; set;
}
public BitmapImage ImageSource
{
get
{
if (MediaSource == null)
{
return null;
}
BitmapImage bitmap = new BitmapImage();
bitmap.SetSource(MediaSource);
return bitmap;
}
}
The first property is the actual media file, saved as a Stream
. We worked with streams a couple of times before (for example to read and write files in the isolated storage). Later, we load this stream inside the MediaElement
(if it’s a movie) or inside the Image
(if it’s, well, an image).
The second property is just a convenience property to make it easier to convert the stream into an image. Should the stream be null
, the property simply returns null
too, and nothing is displayed. If the stream is valid, however, a BitmapImage
is created and loaded with the stream’s content. We use the method SetSource
. This method accepts a stream as input and converts it to an image file.
You can build the application, but running it at this point will create an error, because some properties referenced in the DataTemplates
are now missing or have changed.
Let’s modify the class MediaInformationFile
now. First, we add a new event to notify the Page
(or any subscriber) when the download progress of the Zip file changes. Later we see how that works.
1. In MediaInformationFile
, add the following line under the Loaded
event:
public event DownloadProgressChangedEventHandler DownloadProgressChanged;
2. Replace the last line of the client_DownloadStringCompleted
event handler (the call to OnMediaFileLoaded
) with the code in Listing 22.16:
if (output == null)
{
OnMediaFileLoaded(output, lastError);
}
else
{
LoadZipFile(output);
}
The method LoadZipFile
is called only if the XML file is found and could be parsed. No need to load a big Zip file if there are errors anyway.
3. Implement LoadZipFile
as shown in Listing 22.17:
private void LoadZipFile(IEnumerable<MediaEx> mediaInfos)
{
WebClient client = new WebClient();
client.OpenReadCompleted
+= new OpenReadCompletedEventHandler(
client_OpenReadCompleted);
client.DownloadProgressChanged
+= new DownloadProgressChangedEventHandler(
client_DownloadProgressChanged);
client.OpenReadAsync(new Uri(MEDIA_PATH + ″.zip″, UriKind.Relative),
mediaInfos);
}
We assume the name of the Zip file to be MediaFiles.zip. Here, too, the user must be notified in the user documentation.
The WebClient
is the same class as before, but instead of the method DownloadStringAsync
, we use the more generic OpenReadAsync
. While DownloadStringAsync
can only load text content, OpenReadAsync
can load any file type into a Stream
.
The Completed
event for the OpenReadAsync
method is called OpenReadCompleted
. The event handler signature is different too.
Additionally to the Completed
event, we handle the DownloadProgressChanged
event. This event is raised by the WebClient
class every time a noticeable change occurs in the download progress.
The method OpenReadAsync
is called with a relative URI just like before, but instead of loading the XML file, we now load the Zip file.
We preserved the result of the LINQ query and passed it to the LoadZipFile
method (as the mediaInfos
parameter). Now we preserve it even further: By passing it as the last parameter of the method OpenReadAsync
, we save it as a UserState
. When the response arrives, we can get this object again and use it. This is just a way to save information without having to allocate a private attribute.
4. As we said previously, when the WebClient
warns us about any change in the download progress, we want to notify the Page
. Every time the client’s DownloadProgressChanged
event is raised, we raise our own event. This is why it is declared with the same handler type as the WebClient
class, as shown in Listing 22.18:
void client_DownloadProgressChanged(object sender,
DownloadProgressChangedEventArgs e)
{
if (DownloadProgressChanged != null)
{
DownloadProgressChanged(sender, e);
}
}
Let’s now concentrate on the Completed
event. The event handler’s structure is similar to the one we use for the XML file. Simply add Listing 22.19 to the MediaInformationFile
class:
void client_OpenReadCompleted(object sender,
OpenReadCompletedEventArgs e)
{
MediaExCollection output = new MediaExCollection();
Exception lastError = null;
try
{
// Get media stream here
}
catch (Exception ex)
{
// Failure, inform user
if (ex.InnerException != null
&& ex.InnerException is WebException)
{
lastError = ex.InnerException;
}
else
{
lastError = ex;
}
output = null;
}
OnMediaFileLoaded(output, lastError);
}
Instead of using an IEnumerable
, we now return a MediaExCollection
. This works because MediaExCollection
derives from ObservableCollection<MediaEx>
, and this last class implements IEnumerable<MediaEx>
. So we can say that MediaExCollection
is an IEnumerable<MediaEx>
.
If the Zip file is not found, accessing the property e.Result
throws an exception like with the XML file before.
Replace the line reading // Get media stream here
with the code in Listing 22.20:
StreamResourceInfo zipInfo = new StreamResourceInfo(e.Result, null);
IEnumerable<MediaEx> mediaInfos = e.UserState as IEnumerable<MediaEx>;
foreach (MediaEx mediaInfo in mediaInfos)
{
StreamResourceInfo streamInfo
= Application.GetResourceStream(zipInfo, mediaInfo.FileUri);
mediaInfo.MediaSource = streamInfo.Stream;
output.Add(mediaInfo);
}
Using the property e.Result
(which is now a Stream
, returned by the WebClient
class) we create a new instance of StreamResourceInfo
. This helper class assists us in reading the content of the Zip file.
In e.UserState
, we retrieve the result of the LINQ query that we had saved before by passing it to the OpenReadAsync
method. Since it’s an object
, we need to cast it.
We loop through each MediaEx
instance, retrieve the corresponding Stream
, and then save the instance in the MediaExCollection
.
The static method Application.GetResourceStream
can read inside the Zip file. The first parameter is the StreamResourceInfo
of the Zip file itself. The second parameter is a relative URI pointing to the file we want to extract. The result is another StreamResourceInfo
instance, holding the XML file’s stream as well as other information.
Remember that a Zip file is organized like a tiny file system, with folders and files. Since we took care of preserving the path MediaFiles within the Zip file, the relative URI of the XML file doesn’t change. Only, instead of being relative to the ClientBin folder, it is now relative to the root of the Zip file.
We get a StreamResourceInfo
corresponding to the URI of the current media file. This is yet another use for the handy helper property FileUri
.
The Stream
is directly assigned to our MediaSource
property.
That’s it! The MediaInformationFile
class is now ready to handle Zip files. You can build the application to make sure that everything is fine. The next step is to modify the UI to display the information about the download progress and load the thumbnails using the in-memory Stream.
The user interface requires a few modifications to handle the new functionality. Implement the following steps:
1. In Page.xaml.cs, “hook” the following event handler. Add the code in Listing 22.21 in the event handler Page_Loaded
, just before the call to _mediaFile.LoadMediaFile
:
_mediaFile.DownloadProgressChanged
+= new DownloadProgressChangedEventHandler(
_mediaFile_DownloadProgressChanged);
2. Implement the event handler (Listing 22.22):
void _mediaFile_DownloadProgressChanged(object sender,
DownloadProgressChangedEventArgs e)
{
StatusTextBlock.Text = string.Format(″Loading {0}% ({1} / {2})″,
e.ProgressPercentage, e.BytesReceived, e.TotalBytesToReceive);
}
The DownloadProgressChangedEventArgs
parameter has all kinds of information about the current progress of the download. In this example, we use the ProgressPercentage
property (this is an int
from 0 to 100), the BytesReceived
property (indicating how many bytes
are already available), and the TotalBytesToReceive
property (indicating the total size of the download).
We use the method string.Format
to create the status message. This method’s first parameter is a string
in which you can add placeholders in the form {0}
, {1}
, and so on. When the method is executed, the placeholders are replaced by the second and third parameters, for example.
Using this information, you can also create a progress bar showing a visual indication of the progress. You can see an example in the SDK documentation, under Downloading Content on Demand.
3. In ExpandCollapseMedia, replace the ImageBrush
assignment to ThumbDisplay1.Fill
as shown in Listing 22.23:
if (media is Image)
{
ImageBrush brush = new ImageBrush();
MediaEx context = (media as Image).DataContext as MediaEx;
brush.ImageSource = context.ImageSource;
ThumbDisplay1.Fill = brush;
}
You should be able to build the application now.
We saw that a BitmapImage
can be created from a Stream
using the SetSource
method. The same method is also available in the MediaElement
class. However, this cannot be done in a binding. Instead, we need to create an event handler when each thumbnail is loaded in the UI and call the SetSource
method in the code. Follow these steps:
1. In ThumbnailsViewerControl.xaml, find the DataTemplate
named ThumbnailTemplate
that we use for the ItemsControl
. In the Image
tag, remove the Source
attribute. Instead, add the following event handler:
Loaded=″Media_Loaded″
2. Similarly, remove the Source
attribute from the MediaElement
tag, and replace it with the exact same event handler for Loaded
.
3. Then, in ThumbnailsViewerControl.xaml.cs implement the event handler as shown in Listing 22.24:
private void Media_Loaded(object sender, RoutedEventArgs e)
{
MediaEx context
= (sender as FrameworkElement).DataContext as MediaEx;
if (context.Type == MediaInfo.MediaType.Movie
&& sender is MediaElement)
{
MediaElement senderAsMedia = sender as MediaElement;
senderAsMedia.SetSource(context.MediaSource);
}
if (context.Type == MediaInfo.MediaType.Image
&& sender is Image)
{
Image senderAsImage = sender as Image;
senderAsImage.Source = context.ImageSource;
}
}
Getting the DataContext
(the MediaEx
instance) is something we know how to do.
Depending on the type of media, we can use SetSource
on the MediaElement
(for Movies) or reuse our utility property ImageSource
on the Image
.
You can now run the application. The download progress is displayed, and when 100% is reached, the cache disappears and you can use the application.
Since the Zip file is local when you test, the download progress will move quickly. To make it more obvious and easier to test, you can add a very big file (for example, a big video) to the Zip file. The download will last longer and you can see the progress being updated. Don’t forget to remove that big dummy file, though.
Silverlight is also equipped with more complex request mechanisms. In Chapter 23, we see how to place cross-domain calls to external services, such as Flickr. We can also place calls to web services (either so-called ASMX web services or Windows Communication Foundation [WCF] based). ASMX web services were used a lot before WCF became available. Although Silverlight supports both, and ASMX web services continue to have a bright life, we only demonstrate a WCF-based service here.
In this example, we move the password-checking functionality from the client to the web server and provide a WCF-based service to check it.
The functionality we use to check the user name and password is already defined, but it runs on the web client, in the Silverlight application. As we said already, this is not the cleanest way to handle password check. We will now move this functionality to the web server with the following steps:
1. In Visual Studio, right click on the Thumbnails Solution (not the project!!), which is the first file available on top of the Solution Explorer. Choose Add, New Project.
2. In the Add New Project dialog, select Windows and then Class Library. The library we create now will run on the web server, with the full version of .NET. This is different from a Silverlight class library, which runs with a subset of .NET. Name the new library UserManagementServer.
3. Drag the files User.cs and DataFile.cs from the existing project UserManagement to the new UserManagementServer project. You can also delete the existing file Class1.cs in this server-side project.
4. Once this is done, you can right-click on the UserManagement project and select Remove. We don’t need this project anymore, since we delegate the whole user management to the web server now. Removing the project doesn’t delete it. All the files are still available on your hard drive, but the project is not compiled in this application anymore. If you think you’ll never need this project anymore, you can also delete it from your hard disk (but maybe keeping a backup copy is a good idea!).
With the UserManagement project not available anymore, we need to make a few updates to the Page
class:
1. In the file Page.xaml.cs, remove the namespace UserManagement
from the using
section.
2. Remove the whole content of the method AddUser
except the line return null; // success
.
3. Do the same for the method ValidateUser
(here, too, leave only one line saying return null; // success
).
4. Remove the private attribute _dataFile
from the Page class and delete every statement that references this attribute.
Try to compile the application. You get a series of errors warning you that ″The type or namespace name ′Windows′ does not exist in the namespace ′System′
″. This is correct, because these references to Silverlight libraries are not available on the web server. We don’t need them anyway (in fact, we could very well have removed them before).
5. In DataFile.cs, remove all the statements starting with using System.Windows
. Then do the same operation in the User.cs files.
Our application still cannot be built. A couple of additional changes are needed.
The full version of .NET is outside the scope for this book, so we do not go into too much detail. If you need to write web server code, either in ASP.NET or in Windows Communication Foundation, we recommend other books such as Sams Teach Yourself ASP.NET 2.0 in 24 Hours and WCF Unleashed.
The class DataFile
runs on the web server now, and with the full .NET. We modify it to save files on the hard disk instead of the isolated storage with the following steps.
1. In DataFile.cs, set the path to the user management file to the following. The @
syntax in @″c: empThumbnailsData.3.txt
tells .NET to use the character as a real backslash, instead of an escape sequence.
private const string DATA_FILE_NAME = @″c: empThumbnailsData.3.txt″;
2. The method LoadUsers
now becomes Listing 22.25:
1 public void LoadUsers()
2 {
3 _users = new Dictionary<string, User>();
4
5 if (File.Exists(DATA_FILE_NAME))
6 {
7 using (StreamReader reader = File.OpenText(DATA_FILE_NAME))
8 {
9 string line;
10 do
11 {
12 line = reader.ReadLine();
13 if (line != null)
14 {
15 User newUser = new User(line);
16 _users.Add(newUser.Name, newUser);
17 }
18 }
19 while (line != null);
20 }
21 }
22 }
Instead of reading from the isolated storage, we use the web server’s file system now. Lines 5 to 7 are different than before, but the result is the same: We check whether the user management file exists, and if it does, we open it for reading. The result is a StreamReader
.
The rest of the method is the same as before.
Note that the full version of .NET also has access to an isolated storage located (in our case) on the web server. The syntax to access it is not totally compatible with Silverlight though.
3. The method SaveUsers
also accesses the file system, and must be modified too (Listing 22.26):
1 public void SaveUsers()
2 {
3 using (StreamWriter writer = File.CreateText(DATA_FILE_NAME))
4 {
5 foreach (User user in _users.Values)
6 {
7 writer.WriteLine(user.ToString());
8 }
9 }
10 }
Here, too, the only difference is that we write to the file system instead of the isolated storage. Line 3 creates a StreamWriter
, and the rest of the method is the same.
The rest of the methods remain exactly the same! The application should build now without errors.
As usual, Visual Studio provides great support for creating the new WCF service, as shown with the following steps:
1. In the Thumbnails application, right-click on the project Thumbnails.Web.
2. Select Add New Item from the context menu, and then choose Silverlight-enabled WCF Service. Name the new service UserManagementService.svc.
The service we write now runs on the web server and is written with the whole .NET framework and not “just” Silverlight. It is important that you understand the difference.
Web site project, ASMX and WCF services → web server
Silverlight application and class libraries → web browser
This operation creates a new file named UserManagementService.cs in the App_Code folder. This is a special ASP.NET folder: Each source code file placed into it is compiled on demand as needed. Note that this folder and the source code files it contains must be copied to the web server (this happens automatically when you publish from Visual Studio anyway). The file UserManagementService.cs is the entry point to our WCF service. Note that if you change the name of this WCF service class, you must update the file Web.config. This configuration file defines (among other things) what services are available on the web server.
The functionality to check the user name and password is now available on the server, but we still need to access it from the Silverlight application. This is what the WCF service is for, a gateway to the server-side code. Follow the steps:
1. We need a reference to the server-side library UserManagementServer that we created before. Right click on the Thumbnails.Web project and select Add Reference from the context menu. In the dialog, choose the UserManagementServer from the Projects tab and click OK.
2. In the file UserManagementService.cs, set the Namespace
parameter in the ServiceContract
attribute to something unique. Typically, the parameter is set to a URI, for example http://www.galasoft.ch
.
3. In the same file, add a new class that we use to transfer information from the web server to the client (Listing 22.27). You must add this class below the UserManagementService
. Alternatively, you can also create a new file in the App_Code folder to host this class.
[DataContract]
public class UserInformation
{
[DataMember]
public bool PasswordOk
{
get; internal set;
}
[DataMember]
public DateTime? LastVisit
{
get; internal set;
}
}
The DataContract
attribute warns the framework that this class will be used on the server and on the client. A proxy (or representation) is created for this class in the Silverlight application.
Similarly, the DataMember
attribute specifies which members of this class are made available to the client application. Only public members marked with this attribute are created in the proxy.
4. In the UserManagementService
class, remove the method DoWork
:
5. Implement a new method ValidateUser
as shown in Listing 22.28:
[OperationContract]
public UserInformation ValidateUser(string name, string password)
{
DataFile dataFile = new DataFile();
dataFile.LoadUsers();
UserInformation userInfo = new UserInformation();
User inputUser = new User();
inputUser.Name = name;
inputUser.Password = password;
if (dataFile.ValidateUser(inputUser))
{
User savedUser = dataFile.GetUser(name);
userInfo.LastVisit = savedUser.LastVisit;
userInfo.PasswordOk = true;
savedUser.SetLastVisit();
dataFile.SaveUsers();
}
else
{
userInfo.PasswordOk = false;
}
return userInfo;
}
We pass data to the client in an instance of the UserInformation
class. This data is encoded by the WCF framework and passed to the client. The OperationContract
attribute notifies the framework that this method is available as a service.
6. As for AddUser
, the code is similar to what we did previously in the Page
class. Add the code in Listing 22.29 to the UserManagementService
class.
[OperationContract]
public string AddUser(string name, string password)
{
DataFile dataFile = new DataFile();
dataFile.LoadUsers();
try
{
User newUser = new User();
newUser.Name = name;
newUser.Password = password;
newUser.SetLastVisit();
dataFile.AddUser(newUser);
}
catch (Exception ex)
{
return ex.Message;
}
return null; // success
}
Building the application should work fine now, after adding a using
directive referencing the UserManagement
namespace where the classes DataFile
and User
are placed.
With the web service ready to work, we need to connect the Silverlight application to the new service, with the following steps:
1. Add a Service Reference in our client application. To do this, right click on the Thumbnails project and select Add Service Reference from the context menu.
2. In the next dialog, click on Discover. In the Services panel, you should now see your WCF service.
3. Expand the service by clicking on the small + sign next to it. This operation can take a few moments. After the service expands, you should see the UserManagementService
we created before. What happens here is that Visual Studio asks the WCF service what operations it offers.
4. Enter a name for the new namespace—for example, UserManagement (instead of ServiceReference1). Then click OK. This operation also takes time, as Visual Studio prepares a client-side representation (or proxy) of the web based WCF service.
5. Open Page.xaml.cs and add a private attribute in the Page
class to hold the reference to the service.
private UserManagementServiceClient _serviceClient
= new UserManagementServiceClient();
6. At the end of the Page
constructor, add event handlers for the Completed
events for the two asynchronous methods as in Listing 22.30. As before, Silverlight only allows asynchronous calls to WCF services.
_serviceClient.AddUserCompleted
+= new EventHandler<AddUserCompletedEventArgs>(
_serviceClient_AddUserCompleted);
_serviceClient.ValidateUserCompleted
+= new EventHandler<ValidateUserCompletedEventArgs>(
_serviceClient_ValidateUserCompleted);
For each method marked with the OperationContract
attribute, the framework creates a Completed
event and the corresponding EventArgs
class.
7. The two existing methods AddUser
and ValidateUser
are now just calling the service methods (Listing 22.31):
[ScriptableMember]
public string AddUser(string name, string password)
{
_serviceClient.AddUserAsync(name, password);
return null; // success
}
[ScriptableMember]
public string ValidateUser(string name, string password)
{
_serviceClient.ValidateUserAsync(name, password);
return null;
}
Note that these are asynchronous calls. To keep it simple (well, kind of), we don’t modify the JavaScript method calls, so we simply return null
to indicate that there was no error so far, and that the login dialog should be closed.
8. We still need two event handlers. The first one (for the method ValidateUser
) is shown in Listing 22.32:
void _serviceClient_ValidateUserCompleted(object sender,
ValidateUserCompletedEventArgs e)
{
UserInformation userInfo = e.Result;
if (userInfo.PasswordOk)
{
LastVisitTextBlock.Text = ″(Your last visit was: ″
+ userInfo.LastVisit.Value.ToShortDateString()
+ ″ ″ + userInfo.LastVisit.Value.ToLongTimeString() + ″)″;
}
else
{
LastVisitTextBlock.Text = ″″;
HtmlPage.Window.Alert(″Invalid user/password combination″);
// Here you should probably stop the application!
}
}
The parameter e.Result
is of type UserInformation
. This is the client-side proxy for this server-side class. We have access to all the public members we marked with the DataMember
attribute.
This parameter also provides access to information about the service call: An Error
property indicating whether something bad occurred during the call, a flag (Cancelled
) showing whether the remote call has been cancelled for some reason, and a UserState
property that holds any object you want to save between the moment you send the request and when the response arrives. You can set this object when you call the method on the service client, for example, ValidateUserAsync
.
If the password check fails, we display a JavaScript alert with the corresponding message. Otherwise, we set the LastVisitTextBlock
according to the timestamp we got from the WCF service.
9. And finally, implement the second event handler as shown in Listing 22.33:
void _serviceClient_AddUserCompleted(object sender,
AddUserCompletedEventArgs e)
{
if (e.Result != null)
{
HtmlPage.Window.Alert(e.Result);
}
else
{
LastVisitTextBlock.Text = ″(This is your first visit)″;
}
}
The method AddUser
on the server returns an error message (as a string
) if an error occurred. In that case, we display the error in a JavaScript alert.
At this point, you can run the application and try the login functionality. You will notice a slight delay between the moment you click the Submit button and the application’s reaction. This delay is bigger if you run the application from the web server (use the Publish functionality from Visual Studio to copy all the files there!). The call to a remote method lasts longer. On the other hand, we don’t store any passwords on the client anymore.
Another neat feature of putting the user name and password check on the web server is that the functionality is now consistent on any computer in the world. Since the file is stored on the server, the date of last login will be saved independently of which computer is used to display the application.
In this chapter, we saw multiple ways for a Silverlight application to connect to the web server it originates from. In the next chapter, we will see that the application can even connect to other web servers (in this case, the Flickr photo service).
Silverlight was really developed with connectivity in mind. In fact, what we saw here is only a part of what Silverlight can do. There are built-in classes to handle RSS and Atom feeds (such as those exposed by blog servers), and you can even use sockets. These topics are out of the scope of this book however, and you will find plenty of information about this on the Internet!
This chapter was really advanced and exciting. We opened the doors to the World Wide Web! Or almost: Our Silverlight applications can only connect back to the server-of-origin for the moment. In Chapter 23, we learn how to place requests to other servers and make our Silverlight applications even more connected.
3.145.54.7