Asynchronously Download Content In Silverlight 2

One of the great new features within the world of entertainment has been the addition of on-demand content. This enhancement gives the opportunity to enjoy audio or video elements at your convenience. And although this convenience may be nice in the living room, it's often viewed as a requirement within web-based applications. With this thought in mind, Silverlight gives you the ability to easily retrieve on-demand content with one simple class: WebClient.

The System.Net.WebClient class acts as a special utility class that enables you to asynchronously download content. This class is different from the other networking and communication options in Silverlight in two ways. First, this class provides the ability to monitor the progress of a download request. At the same time, this class uses an event-based model for its API instead of a delegate model used by the other networking and communication options in Silverlight. Regardless, the content you request may be in the form of an image, audio, or video file. Alternatively, this content may be a compressed package of files or even other application modules. Either way, the WebClient class can be used to dynamically retrieve all kinds of content on-demand.

To retrieve items on-demand, you must first request the content. Sometimes, this content can be fairly large in size. Because of this, you must take into consideration how to manage larger download requests. Finally, once a download request has completed, you must decide how you want to load the content. Let's look at the details associated with each of these steps. This content is pulled from the book Silverlight 2 in Action, which provides deep and rich content to help you develop Silverlight 2 applications quickly.

Requesting content

The WebClient class empowers you to request content through two similar, but different, methods. These methods are DownloadStringAsync and OpenReadAsync. The DownloadStringAsync method is intended to be used with string-related data, whereas the OpenReadAsync method is designed to work with binary content. Throughout this section, we'll cover both approaches for requesting content.

Requesting String Content

The DownloadStringAsync method can be used to download string-related content. This content includes things such as JSON, XML, or open text. Regardless of the kind of data, the DownloadStringAsync method will start the download process. This process will run asynchronously until the content is downloaded or an error has occurred. The code to kick off this download process is demonstrated in snippet 1.

Snippet 1

public void RequestStringContent()
{
   Uri uri = new Uri("http://www.somedomain.com/rss.xml");    (1)
   WebClient webClient = new WebClient();
   webClient.DownloadStringAsync(uri);                        (2)
}

This snippet shows how to request string-based content using the WebClient. The first step is to define a Uri that references the content to download (1). This Uri is used later as a parameter to the DownloadStringAsync method (2). This method is responsible for starting the download of the content referenced by the Uri. This content will be retrieved using the HTTP GET method of retrieval. This retrieval method is also used when requesting binary content.

Requesting Binary Content

The OpenReadAsync method can be used to request binary content. This kind of content includes things such as compressed files (.zip files), application modules (other .dll files), and media files. These types of files can be downloaded asynchronously using the OpenReadAsync method as shown in snippet 2.

Snippet 2

public void RequestStreamContent()
{
   Uri uri = new Uri("http://silverlightinaction.com/video3.wmv");
   WebClient webClient = new WebClient();
   webClient.OpenReadAsync(uri);                               (1)
}

This snippet shows how to request binary content using the WebClient class. This process closely resembles the approach used for downloading string content. The only difference is that, for binary content, you should use the OpenReadAsync method (1). Regardless of the type of content, it's important to consider the size of the data. Generally, larger downloads need to provide additional functionality to create a valuable user experience. This feature is part of managing larger download requests.

Managing larger download requests

After a download request has been sent, you need to know when the download has completed. This will be discussed later in this article. For now though, you will see how to keep a user informed as a download progresses. This involves monitoring the progress of a download with the DownloadProgressChanged event. If this download is taking too long, a user may want to cancel the request. For this type of functionality, you can use the CancelAsync method. Both these items are discussed throughout this section.

Monitoring the Progress

The DownloadProgressChanged event empowers you to keep track of the advancement of a download. As a download progresses, the DownloadProgressChanged event will fire repeatedly until all the requested content has been retrieved. When this event is triggered, a DownloadProgressChangedEventArgs parameter is passed to the associated event handler. This parameter gives you access to the details associated with the overall progress of a download. The information provided by the DownloadProgressChangedEventArgs type is demonstrated in snippet 3.

Snippet 3

void webClient_DownloadProgressChanged(object sender,
   DownloadProgressChangedEventArgs e)
{
   StringBuilder sb = new StringBuilder();
   sb.Append(e.BytesReceived + " of ");                        (1)
   sb.Append(e.TotalBytesToReceive + " bytes received");       (2)
   myTextBlock.Text =   sb.ToString();
}

This snippet demonstrates an event handler for the DownloadProgressChanged event. This event handler uses the DownloadProgressChangedEventArgs parameter to build a string informing the user of a download's progress. This string is created by using the two progress-related properties: BytesReceived(1) and TotalBytesToReceive (2).

You may want to show how large a download is. You can accomplish this by using the TotalBytesToReceive property, which is a long value that represents the total number of bytes of the requested content. As this content is retrieved, you can see how many bytes have already been downloaded through the BytesReceived property. This property is a long that gives you the total number of bytes that have been downloaded.

The BytesReceived property, along with the TotalBytesToReceive property, can be valuable in providing updates that can help calm an anxious user. These updates can also be useful in helping a user decide to cancel a download request.

Canceling a Download Request

Providing a user with information about the overall progress of a download can remove the pain of the unknown, but longer downloads can still create an unpleasant experience for a user. If a download isn't a required part of an application, you may want to empower your users with the option to cancel it by calling the CancelAsync method. This method is demonstrated in snippet 4.

Snippet 4

WebClient webClient = new WebClient();
protected void cancelButton_Click(object sender,
   RoutedEventArgs e)                                          (1)
{
   if (webClient.IsBusy)                                       (2)
   {
      webClient.CancelAsync();                                 (3)
   }
}

This snippet shows an event handler for an imaginary Cancel button (1). This event handler cancels a download by using the WebClient class's CancelAsync method (3). This method can be used to halt a web request already in progress. In this particular example, you explicitly check to see if a download is in progress by using the IsBusy property. This property returns a bool value (2) that flags whether or not a download is in progress. In reality, this additional check isn't needed, but it can be used to improve the performance of your code. You can also improve the maintainability of your code by declaring a WebClient as an instance variable.

As you've probably noticed, the CancelAsync method and IsBusy properties are members of the WebClient class, so it's recommended that you define your WebClient class instance as a member of the class associated with your download. This approach is slightly different than that used in snippets 1 and 2. But, by doing this, you have the flexibility to cancel a download request in any way needed. In addition, it gives you the ability to reuse the event handlers associated with a WebClient. Either way, it is important to recognize that a WebClient instance can only handle one download request at a time, so you may want to consider using the IsBusy property before you start a download. Once this download has started and is completed, you must then decide how to load the content.

Asynchronously Download Content In Silverlight 2

Loading the Downloaded Content in Silverlight

When the process of requesting content was discussed, you learned that you have two options. These two options are provided through the DownloadStringAsync and OpenReadAsync methods, which ultimately decide how the downloaded content will be exposed. For instance, if the DownloadStringAsync method is used, the content will become available as a string. Alternatively, if the OpenReadAsync method is used, the content will be unveiled as a Stream. Either way, the requested content doesn't become available until the download request has fully completed.

Once the download request has fully completed, one of the associated completed events will be fired. The specific completed event will be based on the method used to request the content. For example, when the content is requested with the DownloadStringAsync method, the DownloadStringCompleted event will fire. If the OpenReadAsync method is used, the OpenReadCompleted event will be triggered. Either way, both of these event handlers can be used to load the content.

Throughout this section, you'll learn how to load downloaded content. This process involves creating event handlers for the DownloadStringCompleted or OpenReadCompleted event. These event handlers empower you to load all kinds of content including things like string and media items. You can also load fonts, compressed files, and application modules. Occasionally, a cancellation or error may occur before the download has completed. Because of this, we will also discuss how to deal with the termination of a download request.

Loading String Content

Loading string content is an important task in the world of the mashable web. This is important because more often than not, data is exposed through text, JSON, XML, or even XAML. These data formats, among others, can be retrieved using the DownloadStringAsync method. Once this method has completed retrieving the content, you can extract it through a DownloadStringCompleted event handler as shown in Snippet 5.

Snippet 5

private void RequestContent()
{
   Uri address = new Uri(
      "http://ws.geonames.org/findNearByWeatherJSON?
       lat=35.82&lng=-84.04");

   WebClient webClient = new WebClient();
   webClient.DownloadStringCompleted += new
      DownloadStringCompletedEventHandler
         (webClient_DownloadStringCompleted);
   webClient.DownloadStringAsync(address);                     (1)
}

void webClient_DownloadStringCompleted(object sender,          (2)
   DownloadStringCompletedEventArgs e)                         (3)
{
   MessageBox.Show(e.Result);                                  (4)
}

This snippet uses a WebClient to call a web service that returns data in JSON. This web service is called through the DownloadStringAsync method (1). As we discussed earlier, this method asynchronously begins downloading string-related content. When this download has completed, the DownloadStringCompleted event will fire (2). This event passes along the contents of a completed download as a string, which is accessible through the second parameter passed to the DownloadStringCompleted event handler (3). As you've probably noticed, this parameter is represented as a DownloadStringCompletedEventArgs object.

The DownloadStringCompletedEventArgs type exposes the data retrieved from a DownloadStringAsync call. This data can be retrieved through the publicly visible string property, Result (4). This property will give you access to the JSON, XML, XAML, or other text-related response from the initial request. The Result property will also expose a string representation of a binary item if you request one from the DownloadStringAsync method. But, when requesting and loading binary items such as media content, there is a better way.

Loading Media Content

As discussed in chapter 7 of Silverlight 2 in Action, media is an important part of the modern web. This important piece, which includes image, audio, and video files, is generally stored as a binary file on a server. Occasionally, a server may dynamically write this content out over the network. Either way, the string representation created from a DownloadStringAsync call is of little use, so you need a different approach for dynamically loading media content.

To load media content on demand, you must first use the OpenReadAsync method. As shown earlier, this method should be used to download binary content. Once this content has been downloaded, the OpenReadCompleted event will fire. This event gives you the opportunity to load media content via a method called SetSource. This approach is demonstrated in snippet 6.

Snippet 6

private void RequestContent()                                  (1)
{
   Uri address =
      new Uri("http://www.silverlightinaction.com/video2.wmv");
   WebClient webClient = new WebClient();
   webClient.OpenReadCompleted += new
      OpenReadCompletedEventHandler(                           (2)
         webClient_OpenReadCompleted);
   webClient.OpenReadAsync(address);                           (3)
}

void webClient_OpenReadCompleted(object sender,
   OpenReadCompletedEventArgs e)                               (4)
{
   myMediaElement.SetSource(e.Result);                         (5)
}

This snippet loads a video, on demand, when the RequestContent method is called (1). This method requests a video, from a remote server, through the OpenReadAsync method (3). This method downloads the requested video as a Stream, which becomes available as soon as the OpenReadCompleted event is fired (2). When this event is triggered, you can extract the downloaded Stream through the OpenReadCompletedEventArgs property, Result (4). In the case of snippet 11.6, this Stream represents a media file that needs to be loaded.

Loading media content from a Stream involves using a method called SetSource (5). The SetSource method enables you to set the Source property of a media-related item to a downloaded Stream. The Source property of a media-related item generally accepts a Uri, but the SetSource method empowers you to load a Stream. Either way, this approach can be used with a BitmapImage, a MediaElement, or a VideoBrush. Based on these types, it's clear that the SetSource method is intended for on-demand media scenarios. For on-demand font downloading, there's yet another approach.

Loading Fonts

Dynamically loading fonts on demand can add a significant amount of value within the realm of internationalization. When you consider that some eastern fonts can be as large as 25 MB, if not larger, the value begins to become apparent. By using the WebClient class, you can inform the user of the progress of a download of a specialty font. When it comes to specialty fonts, as well as most items, it's important to remember that you may need a license to include them in your application. Once you have permission, you can retrieve fonts on demand using code like that shown in snippet 7.

Snippet 7

private void RequestContent()
{
   Uri address = new Uri("http://www.somedomain.com/somefont.TTF");
   WebClient webClient = new WebClient();
   webClient.OpenReadCompleted += new
      OpenReadCompletedEventHandler(webClient_OpenReadCompleted);
   webClient.OpenReadAsync(address);                           (1)
}

void webClient_OpenReadCompleted(object sender,
   OpenReadCompletedEventArgs e)
{
   FontSource fontSource = new FontSource(e.Result);           (2)
   myTextBlock.FontSource = fontSource;                        (3)
   myTextBlock.FontFamily = new FontFamily("webdings");
}

This snippet dynamically retrieves a font and uses it with a TextBlock. The first step involves using the OpenReadAsync method to request the font (1); this is necessary because a font is generally stored as a binary file. The contents of this file are downloaded as a Stream. Once this Stream has been downloaded, you can load the font. And this is where things get interesting.

Once a font has been downloaded, you need to convert the Stream to a FontSource. Thankfully, the FontSource class's constructor takes a Stream (how convenient!), so you can load up a downloaded Stream as a FontSource (2). This FontSource instance can then be set to the FontSource property of either a TextBlock or TextBox. This snippet uses a TextBlock (3). Once the FontSource property has been set, be sure to set the FontFamily property. This is necessary because a font usually includes multiple typefaces. Once the FontFamily is set, the font of your text element will change.

At this point, you've learned how to dynamically load items one at a time, but often an application will demand several items at once. For instance, you may be creating an application that displays the pictures of a photo album. Because the photo album contains multiple images that will be shown at once, it only makes sense to get them all at the same time. Thankfully, the WebClient makes this task simple with its ability to download compressed packages.

Loading Compressed Packages

Compressed packages are useful for grouping multiple files in an easily transportable format. The most recognizable format, and the one supported within Silverlight, is a .zip file. Because a .zip file is a binary file, you can download one, on demand, from a remote server, using the OpenReadAsync approach. This approach will give you access to a Stream that can be loaded into a StreamResourceInfo. This approach is necessary because the WebClient class does not directly handle compressed files, but the StreamResourceInfo class does, as shown in snippet 8.

Snippet 8

private void RequestContent()
{
   Uri address =
      new Uri("http://www.silverlightinaction.com/files.zip");
   WebClient webClient = new WebClient();
   webClient.OpenReadCompleted += new
      OpenReadCompletedEventHandler(webClient_OpenReadCompleted);
   webClient.OpenReadAsync(address, "man.png");                (1)
}

void webClient_OpenReadCompleted(object sender,
   OpenReadCompletedEventArgs e)
{
   Uri part = new Uri(Convert.ToString(e.UserState),
              UriKind.Relative);                               (2)
   StreamResourceInfo zipStream =                              (3)
      new StreamResourceInfo(e.Result as Stream, null);        (3)
   StreamResourceInfo imageStream =                            (4)
      Application.GetResourceStream(zipStream, part);          (4)

   BitmapImage image = new BitmapImage();
   image.SetSource(imageStream.Stream);
   myImage.Source = image;
}

This snippet demonstrates how to extract a file out of a dynamically downloaded package. The .zip file is requested using the OpenReadAsync method (1). This method takes an additional parameter known as the part. The part represents the name of the file(s) you'd like to extract when the download has completed. This parameter can be any object, so you can pass along a string[] if you want to extract multiple files from a .zip file. Either way, you should understand that these filenames can be relative paths, to files, within a .zip file. This is useful when you actually need to extract a file.

Extracting a file, or files, from a compressed package is a simple three-step process. This process was actually shown in snippet 8. For the sake of reference, the process can be outlined as the following:

  1. Retrieve the part to extract as a relative Uri (2).
  2. Convert the downloaded Stream, which represents a .zip file, into a StreamResourceInfo instance (3).
  3. Extract the individual part using the statically visible Application.GetResourceStream method (4).

Once this process is completed, you're free to use the content as needed. This high-level description of the extraction process is useful for reference. To implement the process, you need to understand the details. Let's begin with the first step.

The first step in extracting a file involves identifying which file to extract. This component is passed in as the part. The part is available through the UserState property of the OpenReadCompletedEventArgs parameter. In snippet 8, the UserState property value is "man.png" because this is what is passed in from the OpenReadAsync method. As we mentioned earlier, this could have been any object, so you may need to create something like a loop if you're using a string[] to extract multiple files. Regardless of the need, ultimately you must convert an individual part to a relative Uri so that you can use it with the downloaded .zip file.

The second step involves getting the downloaded .zip file. This .zip file is available through the Result property of the OpenReadCompletedEventArgs parameter. You need to convert this Stream into a StreamResourceInfo to get to the part. Once again, this approach is necessary because the WebClient class doesn't directly support .zip flies, but you can convert a Stream into a StreamResourceInfo using the default constructor. This constructor takes a second parameter that represents the MIME type of the content. In the case of a .zip file, you can just pass in null.

The final step involves actually extracting the part. You can accomplish this through the statically visible method Application.GetResourceStream, which returns an individual file from a .zip file. The StreamResourceInfo that represents the .zip file is passed as the first parameter to this method. Meanwhile, the relative Uri that points to a file within the .zip file is passed as the second parameter. If this file doesn't exist within the .zip file, the method will throw an IndexOutOfRangeException. If the file does exist, it will be returned in the form of a StreamResourceInfo that can be used as needed.

These three steps empower you to easily load groupings of files on demand. Generally these files will include things like images, media files, or XAML content. Occasionally, you may need to retrieve additional UI components, such as the Silverlight Toolkit, on the fly. Alternatively, you may need to download complex application logic. Either way, these items are generally stored in application libraries. For these situations, it's useful to know how to use the WebClient class to load additional application modules.

Asynchronously Download Content In Silverlight 2

Loading Application Modules

Splitting an application into multiple application modules can make it more manageable. These application modules represent class libraries that may or may not be packaged in an application's .xap file. When a class library is in a .xap file, you can reference it like any other .NET project. When you add an application module though, you also increase the size of the resulting .xap file. This can cause unnecessary wait times for your users. Luckily, there's a more elegant alternative.

Silverlight gives you the flexibility to create applications that can download modules on-demand. To download a module on-demand, you create a class library in the usual fashion, but this class library should not be referenced by your Silverlight application. Instead, you must upload the class library to a hosting server and then download it using the WebClient class. This class will give you access to the downloaded library. You can then use this library through reflection, as shown in snippet 9.

Snippet 9

private void RequestContent()
{
   Uri address =
      new Uri("http://www.somedomain.com/MyClassLibrary.dll");

   WebClient webClient = new WebClient();
   webClient.OpenReadCompleted += new
      OpenReadCompletedEventHandler(webClient_OpenReadCompleted);
   webClient.OpenReadAsync(address);                           (1)
}

void webClient_OpenReadCompleted(object sender,
   OpenReadCompletedEventArgs e)
{
   AssemblyPart assemblyPart = new AssemblyPart();             (2)
   Assembly assembly = assemblyPart.Load(e.Result);            (3)

   object myClass =
      assembly.CreateInstance("MyClassLibrary.MyClass");       (4)
   object result = myClass.GetType().InvokeMember(             (4)
      "GetCurrentTime", BindingFlags.InvokeMethod, null,
      myClass, null);                                          (4)
   myTextBlock.Text = Convert.ToString(result);                (4)
}

This snippet shows how to dynamically download, and use, an application module. An application module is a type of binary file, so you can download it with our old friend, OpenReadAsync (1). When this method has completed, the requested class library will be accessible through the downloaded Stream. But, before you can use the contents of the class library, you need to convert it to an Assembly.

Because Assembly is an in-memory representation of a component, it can be loaded into memory through an instance of the AssemblyPart class (2). This class represents an assembly that should be included with an application's package (the .xap file). To include an assembly, you pass the downloaded Stream to the Load method of an AssemblyPart (3). This method will introduce the new class library into the current application domain.

Once an Assembly is loaded into the application domain, you can use its contents. These contents include things like classes, enums, and structs. In turn, these items may have members like methods, properties, and events. You can invoke these members through reflection, as shown in (4) of this snippet. This powerful technique is a staple of the .NET Framework, so it's slightly outside of the scope of this book, but you can definitely find out more in any .NET Framework book. The important thing to acknowledge is that this approach is used to leverage the contents of a class library downloaded on-demand. Occasionally, you may have difficulties in downloading items on-demand. This can occur because the request was somehow terminated. Let's look at how to deal with request termination.

Dealing with Termination

On-demand download requests may be stopped either intentionally or unintentionally. An intentional termination occurs when a user decides to cancel an asynchronous request. We discussed this in section 11.4.2. Alternatively, a download request may fail due to unforeseen circumstances. Either way, it's your responsibility to make sure a download successfully completed before attempting to use the downloaded content in snippet 10.

Snippet 10

void webClient_OpenReadCompleted(object sender,
   OpenReadCompletedEventArgs e)                               (1)
{
   if ((e.Cancelled == false) && (e.Error == null))            (2)
   {
      myTextBlock.Text = "Download Succeeded!";
   }
}

This snippet demonstrates how to check if a download was successful. This specific example works in response to a request started by the OpenReadAsync method. You can tell because of the OpenReadCompletedEventArgs parameter . This same approach can be used if a request was started from DownloadStringAsync because both the DownloadStringCompletedEventArgs and OpenReadCompletedEventArgs types derive from the AsyncCompletedEventArgs type. This type exposes the two properties used in this snippet: Cancelled and Error (2).

The Cancelled property is a bool that lets you know if a download request was halted. If so, this property will be true; otherwise, it will be false. Incidentally, the Cancelled property will be false if an error has occurred. In this case, the Error property will contain an Exception that details the problem. If everything is okay, the Cancelled property will be false and the Error property will be null as shown in snippet 10.

In Conclusion

Downloading application modules when needed can enhance a user's experience through reducing load times. This powerful addition can be used when retrieving fonts, media, and textual content. Alternatively, you can download collections of these elements as compressed packages. Either way, downloading content on demand is a valuable runtime feature.



Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: May 7, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT This eSeminar will explore three popular games engines and how they empower developers to create exciting, graphically rich, and high-performance games for Android® on Intel® Architecture. Join us for a deep dive as experts describe the features, tools, and common challenges using Marmalade, App Game Kit, and Havok game engines, as well as a discussion of the pros and cons of each engine and how they fit into your development …

  • Download the Information Governance Survey Benchmark Report to gain insights that can help you further establish business value in your Records and Information Management (RIM) program and across your entire organization. Discover how your peers in the industry are dealing with this evolving information lifecycle management environment and uncover key insights such as: 87% of organizations surveyed have a RIM program in place 8% measure compliance 64% cannot get employees to "let go" of information for …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds