Chapter 22 Connecting to the Web

In This Chapter

Creating and Loading an XML File

Loading a Zip File and Showing Progress

Making Requests to WCF Services

More Connectivity

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.

Creating and Loading an XML File

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.

Removing the Media Information from the Application

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.

Listing 22.1 XML Media Information File

<?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.

Showing and Hiding a Startup Screen

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:

Listing 22.2 Cache Panel

<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:

Listing 22.3 Storyboard to Hide the Cache

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

Loading the XML Media Information File

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.

Listing 22.4 MediaFileLoadedEventArgs Class

public class MediaFileLoadedEventArgs : EventArgs
{
    public IEnumerable<MediaEx> Collection
    {
        getset;
    }
    public Exception Error
    {
        getset;
    }
}

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:

Listing 22.5 Loaded Event and Companion Method

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

Image 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).

Image 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.

Sending the Request

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:

Listing 22.6 LoadMediaFile Method

  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  }

Image 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.

Image 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.

Image 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.

Image 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.

Image 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).

Image 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.

Getting a Response

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.

Listing 22.7 client_DownloadStringCompleted Event Handler

  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  }

Image 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.

Image On line 6 we create a variable for an error that could occur when loading the XML file.

Image 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.

Image 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.

Reading an XML File with LINQ

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.

Listing 22.8 Making a LINQ Query

  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 = (MediaTypeEnum.Parse(typeof(MediaType),
10          (string) xmlMediaInfo.Attribute(″type″), true),
11      Description = (string) xmlMediaInfo.Attribute(″description″)
12    };

Image 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.

Image 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.

Image 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.

Image 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.

Image 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.

Image 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.

Image 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.

Image Line 7 sets the MediaPath property to the constant we declared before.

Image Line 8 gets the name attribute from the XML element and assigns it to the MediaName property.

Image 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.”

Image Finally, the Description is also a string, so it is read directly from the XML element.

Parsing Enums

The method Enum.Parse is complex, so let’s review it:

Image The first parameter of this method is the type of enumeration we want to get, for example typeof(MediaType).

Image 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.

Image 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.

Image The Parse method returns an object that needs to be casted to the desired enumeration type.

Triggering the Request

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):

Listing 22.9 Page_Loaded Event Handler

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.

Handling the Results

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.

Listing 22.10 _mediaFile_Loaded 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.

Listing 22.11 Error Handling

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.

Listing 22.12 Enumerating the Items and Setting Brushes

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

Image Adding the items to the collection is straightforward since LINQ created them already of the right type. No need to cast.

Image 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.

Listing 22.13 Getting and Starting the Storyboard

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:

Listing 22.14 cacheFadeOut_Completed Event Handler

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.

Testing the Application

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.

Loading a Zip File and Showing Progress

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.

Image Handling multiple smaller responses might be more suited for slow connections, while one big download is better for a faster line.

Image 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.

Image 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.

Image 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.

Creating the Zip File

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.

Extending the MediaEx Class to Store the Stream

The 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:

Listing 22.15 Two New Properties

public Stream MediaSource
{
    getset;
}
public BitmapImage ImageSource
{
    get
    {
        if (MediaSource == null)
       {
           return null;
        }
        BitmapImage bitmap = new BitmapImage();
        bitmap.SetSource(MediaSource);
        return bitmap;
    }
}

Image 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).

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.

Loading the Zip File

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:

Listing 22.16 Loading the Zip File

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:

Listing 22.17 LoadZipFile Method

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

Image We assume the name of the Zip file to be MediaFiles.zip. Here, too, the user must be notified in the user documentation.

Image 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.

Image The Completed event for the OpenReadAsync method is called OpenReadCompleted. The event handler signature is different too.

Image 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.

Image 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.

Image 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:

Listing 22.18 client_DownloadProgressChanged Event Handler

void client_DownloadProgressChanged(object sender,
    DownloadProgressChangedEventArgs e)
{
    if (DownloadProgressChanged != null)
   {
      DownloadProgressChanged(sender, e);
   }
}

Reading the Zipped Files

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:

Listing 22.19 client_OpenReadCompleted Event Handler

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

Image 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>.

Image 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:

Listing 22.20 client_OpenReadCompleted Event Handler

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

Image 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.

Image 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.

Image We loop through each MediaEx instance, retrieve the corresponding Stream, and then save the instance in the MediaExCollection.

Image 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.

Image 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.

Image We get a StreamResourceInfo corresponding to the URI of the current media file. This is yet another use for the handy helper property FileUri.

Image 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.

Updating the UI

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:

Listing 22.21 Adding an Event Handler

_mediaFile.DownloadProgressChanged
    += new DownloadProgressChangedEventHandler(
       _mediaFile_DownloadProgressChanged);

2. Implement the event handler (Listing 22.22):

Listing 22.22 mediaFile_DownloadProgressChanged Handler

void _mediaFile_DownloadProgressChanged(object sender,
    DownloadProgressChangedEventArgs e)
{
    StatusTextBlock.Text = string.Format(″Loading {0}% ({1} / {2})″,
       e.ProgressPercentage, e.BytesReceived, e.TotalBytesToReceive);
}

Image 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).

Image 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.

Image 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:

Listing 22.23 Setting the ThumbDisplay1

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:

Listing 22.24 Media_Loaded Event Handler

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

Image Getting the DataContext (the MediaEx instance) is something we know how to do.

Image 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.

Making Requests to WCF Services

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.

Moving the User and DataFile Classes to the Server

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.

Changing the DataFile to Run on the Server

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:

Listing 22.25 LoadUsers Method

  1  public void LoadUsers()
  2  {
  3    _users = new Dictionary<stringUser>();
  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  }

Image 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.

Image The rest of the method is the same as before.

Image 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):

Listing 22.26 SaveUsers Method

  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.

Creating the WCF Service

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.

Image Web site project, ASMX and WCF services → web server

Image 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.

Implementing the Service

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.

Listing 22.27 UserInformation Class

[DataContract]
public class UserInformation
{
    [DataMember]
    public bool PasswordOk
    {
        get; internal set;
    }

    [DataMember]
    public DateTime? LastVisit
    {
        get; internal set;
    }
}

Image 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.

Image 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:

Listing 22.28 ValidateUser Method

[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.

Listing 22.29 AddUser Method

[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.

Updating the Client Application

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.

Listing 22.30 Adding Event Handlers

_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):

Listing 22.31 AddUser and ValidateUser Methods

[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:

Listing 22.32 ValidateUserCompleted Event Handler

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!
    }
}

Image 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.

Image 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.

Image 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:

Listing 22.33 AddUserCompleted Event Handler

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.

More Connectivity

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!

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.54.7