|This chapter covers|
Throughout this book, you’ve witnessed how to use Silverlight to create RIAs, which clearly extend beyond the traditional web experiences available with AJAX. To make these newfound treasures really sparkle, you need to take advantage of Silverlight’s runtime features.
Throughout this chapter, you’ll learn how to enrich the user’s experience using Silverlight’s runtime features. These features give you the ability to store and retrieve data on the user’s machine through isolated storage. In addition, we’ll discuss the process of loading XAML, processing data, and downloading files without interrupting the UI. We’ll end this chapter by discussing the incredibly powerful Dynamic Language Runtime (DLR)
Silverlight is a browser-based plug-in, so it has the lowest of security privileges to ensure a safe browsing experience for your users. This safety restriction introduces a number of development challenges such as working with data across browser sessions. Although working with data across browser sessions may not be a problem if the data is stored on a web server, it can be a problem if the data needs to be stored locally.
Isolated storage is a mechanism that allows you to preserve data, across browser sessions, on a user’s machine. This storage area is tied to an individual user and helps you overcome the 4kb limitation of a cookie. Unlike a cookie, isolated storage lies outside of the browser cache—if a user clears the browser history, the items within isolated storage will remain in place. In order to access this storage area, you use the System.IO.IsolatedStorage namespace.
The System.IO.IsolatedStorage namespace provides the functionality to work with a user’s isolated storage area. This area can be accessed through the valuable IsolatedStorageFile class, which exposes two statically visible methods that retrieve an IsolatedStorageFile. These methods are GetUserStoreForApplication and GetUserStoreForSite. The GetUserStoreForApplication can be used to retrieve a user’s isolated storage for a specific application. The GetUserStoreForSite method gets a user’s isolated storage for an entire domain. As you may have guessed, this method gives you the ability to share information across multiple Silverlight applications.
The GetUserStoreForSite method doesn’t exist in the full .NET frame-work. You should consider this fact if you’d like to promote your Silver-light application to WPF down the road.
Either way, an example of retrieving an IsolatedStorageFile is shown in snippet 11.1.
IsolatedStorageFile isoFile =
This code gives you access to the isolated storage area for a user. Once you’ve retrieved an IsolatedStorageFile, you can use it to manage a virtual filesystem, which gives you the ability to read and write files and directories that may contain valuable information. This information can be leveraged through the IsolatedStorageFile and IsolatedStorageFileStream classes.
The IsolatedStorageFile class represents a virtual filesystem that a Silverlight application can manage. Note the word virtual; you can’t directly access the user’s local file-system due to security constraints. As snippet 11.1 showed, you can still access data related to a requesting Silverlight application, but in reality, the term filesystem is a probably a bit of a stretch.
The IsolatedStorageFile object represents a specific partition within the isolated storage area. This partition is tied to both the user and the application. It’s easiest to think of this partition as a specific folder or directory. And, like a regular directory, the isolated storage area enables you to perform several valuable operations including the ability to list the contents of a directory. This directory can have other files or directories added or removed from it, so you should probably keep track of the isolated storage usage statistics. Fortunately, the IsolatedStorageFile allows you to check these statistics and request more space if you need it.
The IsolatedStorageFile class provides two methods that enable you to retrieve the items within a storage area. The first method, GetDirectoryNames, enables you to retrieve the names of the directories that match a certain pattern; the GetFileNames method allows you to search for files that match a particular filter. To gain a solid understanding of how these filters work, look at the example isolated storage area structure (figure 11.1).
The isolated storage area depicted in figure 11.1 contains a number of common filesystem items. For instance, there are three text files, one xml file, and one subdirectory. With this hierarchical structure in mind, let’s turn our focus to mastering the filtering string syntax used for searching the isolated storage area.
The first and most verbose approach involves searching for a specifically named item. This approach works with both the GetDirectoryNames and GetFileNames methods. To perform the search, you simply provide the exact path to the file or directory. If the filename or directory is found, a string array with one element will be returned. Otherwise, an empty result set will be returned. Both approaches are shown in snippet 11.2.
string directory1 = isoFile.GetDirectoryNames("Directory1");
string noDirFound = isoFile.GetDirectoryNames("Directory2");
string testfile1 = isoFile.GetFileNames("testfile1.txt");
string noFileFound = isoFile.GetFileNames("testfile2.txt");
string nestedFile = isoFile.GetFileNames("Directory1/file1.txt");
As this snippet shows, you can easily retrieve both directory and file names using an absolute syntax. The real power of the filtering string comes through with the use of the * and ? wildcard characters.
The * wildcard character is used to search for any character. For example, you can use this approach to search for files or directories that begin with a specific letter or, perhaps, retrieve all the files within a subdirectory. At the same time, you can use this approach to search for files of a specific type. All these possibilities are shown in snippet 11.3.
string results1 = isoFile.GetFileNames("*");
string results2 = isoFile.GetFileNames("Directory1/*");
string results3 = isoFile.GetFileNames("textfile*");
string results4 = isoFile.GetFileNames("*.txt");
This snippet illustrates the flexibility associated with using the * wildcard character. Although it isn’t shown, the * wildcard character is applicable within the GetDirectoryNames method as well. Also applicable to the GetDirectoryNames method is the ? wildcard character.
Table 11.1. Using the ? wildcard character in relation to figure 11.1
|string results1 = isoFile.GetFileNames("te?tfile1.txt");||testfile1.txt textfile1.txt|
|string results2 = isoFile.GetFileNames("te?tfile*");||testfile1.txt textfile1.txt textfile2.xml|
This table demonstrates how the ? wildcard works. This character, in coordination with the *, provides a way to shortcut your file and directory querying tasks. These tasks can give you visibility to items available within an isolated storage area. Occasionally, some of these items may need to be removed
The IsolatedStorageFile class exposes two utility methods that empower you to remove items from the storage area. The first method, DeleteDirectory, is used to remove a directory from the isolated storage area. The other method, DeleteFile, similarly allows you to remove a file. The usage of the DeleteFile method is illustrated in snippet 11.4.
As this snippet shows, you must explicitly provide the absolute path to the file you wish to delete. If you provide an invalid path, an IsolatedStorageException will be thrown. In addition, this same exception will be thrown if you attempt to remove a directory that isn’t empty. That’s right; a directory must be completely empty before you delete it. Other than that, the syntax is the same when using the DeleteDirectory method. But, before you can delete a directory, it needs to be created.
The IsolatedStorageFile class exposes a method called CreateDirectory that enables you to create a directory within the isolated storage space. There isn’t anything too shocking about the syntax associated with this method—to create a directory, you state the name of the folder (snippet 11.5).
In addition to creating directories at the root of the isolated storage area, the Create-Directory method enables you to create subdirectories. To do this, you use a URL-style syntax that uses forward slashes as separators. Snippet 11.6 shows the process of creating two subdirectories.
The first line of code is pretty simple. The second line of code shows an additional feature . If you provide an absolute path to a subdirectory further down the line, all missing directories along the way will automatically be added. For instance, based on figure 11.1, the subdirectory Sub2 doesn’t exist before the second line of code from snippet 11.6. When the call to create the Leaf subdirectory occurs, the Sub2 subdirectory is automatically created so that Leaf has a place to live. Once a directory exists, you can add files to it. We’ll discuss adding files in section 11.1.2. But first, let’s make sure there’s space for a new file.
The IsolatedStorageFile class exposes two read-only properties that inform you of an isolated storage area’s memory situation. The first property, Quota, holds the total number of bytes allocated to the storage area. The other property, Available-FreeSpace, represents the number of bytes remaining in the storage area. You can use these properties together to create a cool little memory quota bar (snippet 11.7).
Snippet 11.7. C# XAML Result: Creating a file quota bar associated with the user’s isolated storage area
This snippet shows one way you can put the AvailableFreeSpace and Quota properties to use. These properties are used to set the widths of the Rectangle elements based on the available and used space . In this example, we divided these values by 10024 and then multiplied by 2 to create a reasonably sized quota bar. The value of 10024 has little significance, but you may be wondering how to use the Quota property is determined.
By default, the Quota property value is set to 1048576. The reason why is because by default, each isolated storage area is given 1MB of space. If you remember that the Quota property represents the number of bytes allocated to an isolated storage area, you can see how 1048576 bytes = 1024kb = 1MB. Significantly though, you have the option to ask the user for more space should your application need it.
The IsolatedStorageFile class empowers you to ask the user for more storage space. This request can be made by calling the IncreaseQuotaTo method, which accepts a long parameter that represents the new quota size you want. This size signals the total number of bytes you want to allocate to the isolated storage area; it doesn’t represent the number of bytes you want to increase by. When the IncreaseQuotaTo method is called, the user will be shown a dialog box, as shown in snippet 11.8.
Snippet 11.8. C# Result: Requesting more isolated storage space. This specific dialog box asks the user if the Silverlight application can double the amount of available isolated storage.
IsolatedStorageFile isoFile =
long newQuotaSize = isoFile.Quota * 2;
bool requestAccepted = isoFile.IncreaseQuotaTo(newQuotaSize);
This snippet shows how to request, from a user, more space for an application. You also have the option of asking for more storage for a domain if you retrieve the IsolatedStorageFile through the GetUserStoreForSite method. Either way, the IncreaseQuotaTo method can only be called from a user-initiated event such as a button click event. Once this request is made, the dialog box shown in snippet 11.8 will be displayed to the user. This dialog box displays the name of the website requesting the new quota. This value is automatically set behind the scenes to prevent malicious coding. In addition, this dialog box shows how much space is currently being used, as well as the quota size being requested. This request can be accepted or denied by the user. The user’s decision will be returned from the IncreaseQuotaTo method in the form of a bool. This helps you act according to a user’s decision because, ultimately, the user controls the virtual filesystem.
The IsolatedStorageFile represents a virtual filesystem. This filesystem gives you the flexibility to create, navigate, and remove items from within it. To make sure that you have space to create items, you may need to check the AvailableFreeSpace property, which represents the number of bytes available within the allocated storage Quota. This Quota is set to 1MB by default. If you need more space you can request it by using the IncreaseQuotaTo method. Requesting more space can come in handy as you read and write files.
Files stored within the isolated storage area can be created and retrieved through a file stream. This file IO task is like any other in the .NET framework, but because you’re working within a special area that provides additional security features, you must use a specific type of file stream. This particular type of file stream is appropriately named IsolatedStorageFileStream.
The IsolatedStorageFileStream object provides in-memory access to a file stored within the isolated storage area. With this object, you can create, update, and read a file from the isolated storage area. Because a file must exist before you can read it, it only makes sense to first discuss how to create and update files within isolated storage.
Creating a file within a user’s isolated storage area is a simple process. This process hinges on the usage of a System.IO.StreamWriter object. You can use a Stream-Writer to write content into a file stored within isolated storage. Snippet 11.9 shows the process of writing a basic text file to the user’s isolated storage area.
This snippet shows how easily you can write a text file into the isolated storage area. The first step is to retrieve a user’s isolated storage area . We discussed this in section 11.1. Then, you create an IsolatedStorageFileStream that represents a file within isolated storage . The contents of this file are created using a StreamWriter . This StreamWriter gives you the flexibility to write either binary data or regular text. This is important to recognize because the contents of an isolated storage area aren’t encrypted. Because of this, you may want to encrypt your data when writing it into a file.
You may have noticed the use of the FileMode enumerator in of snippet 11.9. This value determines how the file will be opened. In all, there are six different ways to open a file. All six options are explained in table 11.2.
The FileMode options shown in this table cover a wide variety of file operations. These values are useful when you’re creating files. These options are also useful when you’re attempting to read a file from isolated storage.
The process of reading a file from a user’s isolated storage area is similar to writing to a file. Instead of taking advantage of a StreamWriter, you employ the use of a Stream-Reader. The process of using a StreamReader to read a file is shown in snippet 11.10.
Snippet 11.10. C#: Reading a file from a user’s isolated storage area. The contents of the file are shown through a pretend TextBlock.
As this snippet shows, reading a file is almost identical to creating a file. The first step involves retrieving the user’s isolated storage area . Then, you create an Isolated-StorageFileStream object—this time using the FileMode.Open option . Once the file is opened, you can read through it using a StreamReader . Believe it or not, that’s all there is to it!
Both the StreamReader and StreamWriter classes provide a lot of rich features for working with character-based input and output. These IO features provide a lot of flexibility in regard to client-side storage within the isolated storage area. Once an isolated storage area is created, you may need to remove it for testing during development. For this reason, it is beneficial to know how to administer it.
Administering an isolated storage area involves interacting with the physical file-system. The reason you’d want to do this is to test a user’s initial interaction with a Silver-light application. During development, it can be easy to get lost in the action and forget a user’s initial experience with an application. Because the isolated storage area is separate from the browser’s cache, you need an easy way to remove information from the isolated storage area, so you should know where the isolated storage area is located on the physical filesystem.
The isolated storage area is located in different locations based on the user’s operating system. The specific location for each operating system is shown in table 11.3.
Table 11.3. The base location of the isolated storage area on each operating system supported in Silverlight 2
|Mac OS X||AppData\Local|
|Windows XP||C:\Documents and Settings\[UserName]\Application Data\Microsoft\Silverlight\is|
This table shows the base location for the isolated storage area. Each unique Silver-light application that uses isolated storage will create a new directory under this location. This new directory will be given a name that appears encrypted, but don’t let this fool you. The data stored in the isolated storage area is not encrypted, so you shouldn’t store sensitive information, such as passwords, in isolated storage.
In general, it’s nice to know there’s a way of quickly testing a user’s initial experience with your isolated storage features. It’s also nice to know that, in general, there’s a way to store information on a client’s machine. This information could be something like basic application settings for a user or something as involved as XAML that defines part of the user experience. Let’s look at how you can use XAML at runtime.
In chapter 1, we gave a quick overview of the features of XAML. At this point, I hope you have a clearer understanding of what XAML is. Essentially, XAML is a serialized format for in-memory objects. This is important to understand because there are ways to convert from one format to the other. This powerful technique can be used to deliver customized user experiences that can be preserved across sessions, but first you must understand how to load XAML at runtime.
The process of loading XAML at runtime is incredibly easy. In fact, you only need to rely on the XamlReader class, which belongs to the System.Windows.Markup namespace. This class empowers you to parse XAML and convert it into an in-memory object. This object can be created by a statically visible method called Load. This method takes a string of XAML and converts it to the appropriate object. Then you can insert this object into another UIElement. Snippet 11.11 shows this entire process in action.
string xaml = "<Rectangle
Height=\"60\" Width=\"20\" Fill=\"Blue\" />";
Rectangle rectangle = (Rectangle) XamlReader.Load(xaml);
This snippet creates a Rectangle from a string of XAML . The Rectangle will be created by the Load method of the XamlReader as long as a default XML namespace is set. This default namespace is set in this snippet by the xmlns namespace of the XAML snippet. It’s important to acknowledge that the root element must specify an XML namespace. Once the Rectangle is created, it’s added to the assumed Canvas . When this addition happens, the Rectangle will be shown within the Silverlight application.
The Load method in this snippet also demonstrates an important fact: The Load method can only return the object representation for one XAML element at a time. This means that multiple Rectangle elements would not have been permitted in this snippet. In reality, if this would have been attempted, an XamlParseException would have been thrown. But you can use one element with nested elements, so the root XAML element of your string will generally represent one of the Panel options discussed in chapter 3. This approach is shown in snippet 11.12.
string xaml = "<Grid
"<ColumnDefinition /><ColumnDefinition /></Grid.ColumnDefinitions>" +
"<Grid.RowDefinitions><RowDefinition /><RowDefinition />" +
"</Grid.RowDefinitions><TextBlock Text=\"Name: \" />" +
"<TextBox Width=\"80\" Height=\"20\" Grid.Column=\"1\" />" +
"<TextBlock Text=\"Email Address: \" Grid.Row=\"1\" />" +
"<TextBox Grid.Row=\"1\" Grid.Column=\"1\" Width=\"80\" Height=\"20\" /
Grid myGrid = (Grid) XamlReader.Load(xaml);
This snippet adds a Grid to an assumed Canvas. This Grid contains a variety of other valuable elements. This approach shows the power of using nested elements. As you can imagine, this string of XAML can grow to be quite complex. In those situations, you may want to use the save features discussed in 11.1 to save all that hard work. When doing this, you may choose to use a web service to send the XAML to a robust data source. Unfortunately, calling a web service can be a time-consuming process, which can cause your UI to hang. Thankfully, there’s a component perfectly suited for handling time-consuming processes. This component is called the BackgroundWorker.
The System.ComponentModel namespace provides a class called BackgroundWorker that can be used to perform a task behind the scenes. This appropriately named class enables you to asynchronously perform a task on a thread separate from the UI thread. For this reason, the BackgroundWorker is useful for web-service calls, complex calculations, or other time-consuming operations. In general, the BackgroundWorker is useful for handling tasks that would normally cause your UI to hang.
When you encounter these types of tasks, you can implement a BackgroundWorker using a few simple steps. These steps involve identifying what task you wish to perform and when your UI should be updated during the task. For example, imagine you’re performing some time-consuming task and you want to present a ProgressBar to your users to inform them of the progress of the task. The XAML for this ProgressBar is defined in snippet 11.13.
Snippet 11.13. XAML Result: The XAML for the progress bar. The figure shown is simply for illustration. This illustration represents a task halfway through the completion.
Height="30" Width="250" Background="White"
<LinearGradientBrush EndPoint="0.5,1.35" StartPoint="0.5,-0.3">
<GradientStop Color="#FF67E206" Offset="0"/>
<GradientStop Color="#FF209900" Offset="0.418"/>
<GradientStop Color="#FF67E206" Offset="1"/>
As this snippet shows, you can easily create the look of a ProgressBar using XAML and/or Blend. But to implement the behavior of the ProgressBar, you first need to identify the task you’re performing. We’ll use the XAML in snippet 11.13 as our UI for the remainder of section 11.3. The next natural step involves implementing the code responsible for doing the work.
To do the work, you have to create an instance of the BackgroundWorker class. After this is completed, you must add an event handler to the specific instance of the BackgroundWorker class. This event handler will do the work associated with the task. This appropriately named event is called DoWork and is shown in snippet 11.14.
In this snippet, you’ll notice that a BackgroundWorker variable is defined at the class-scope level , so you’re able to interact with its features across the various methods and events, such as the one shown in . This particular event wire-up is used to run the code shown in . This code asks the thread to sleep for 25 milliseconds, 100 times, to simulate a time-intensive task. This task is started once the RunWorkerAsync method is called .
The RunWorkerAsync method is responsible for beginning the background task asynchronously. This method provides an overload that enables you to pass an argument to the DoWork event handler. This argument can be virtually anything you may need for your task. For instance, you may use it to pass a seed for a random number generator. This argument is then accessible through the Argument property of the DoWorkEventArgs parameter of the DoWork event handler. Once the RunWorkerAsync method is called, the code in the DoWork event handler will begin execution.
The code within the DoWork event handler executes on a thread separate from the UI thread. This is very important to understand because this thread cannot update the UI—the DoWork event handler can’t change anything that you’ve defined within your XAML code. If you attempt to update the UI from the DoWork thread you’ll get an UnauthorizedAccessException stating, Invalid cross-thread access. To overcome this potential pitfall, you must rely on the other events associated with the Background-Worker to update the UI.
The BackgroundWorker class exposes two events that can be used to update the UI. These two events are ProgressChanged and RunWorkerCompleted. Both events have the ability to update the UI because they run in the same thread as the UI, so you shouldn’t perform any time-consuming tasks within these events. If you do, it can cause your UI to hang and create less-than-desirable experiences for your users. But, you can use these events to enhance your users’ experiences by giving them updates related to the progress of the BackgroundWorker task. These updates include progress changes, task cancellation, and task completion.
Sending progress updates to the UI from a BackgroundWorker involves three basic steps. The first step involves telling the BackgroundWorker to report progress changes. Then, an event handler must be created to respond to the progress changes. Finally, it’s your responsibility to periodically report when the task progress changes. These three steps are shown in snippet 11.15.
This snippet shows how to report the progress of a task from a BackgroundWorker. The first step in the process involves setting the WorkerReportsProgress property to true . By default, this property is set to false. By setting this property to true, you inform the BackgroundWorker that you’re going to implement a ProgressChanged event .
The ProgressChanged event is responsible for handling reported progress changes. These changes can be reported through the ReportProgress method . This method serves as the bridge between the background thread and the UI thread. As you can see, this method is periodically called from within the DoWork event handler . When this call is made, an integer value between 0 and 100 is sent, signaling the percentage of the task completed. This percentage is then accessible through the ProgressChangedEventArgs parameter of the ProgressChanged event.
This simple three-step process can enhance a user’s experience with your application. This approach can be valuable in times when you’re working with a time-consuming task. Snippet 11.15 imitated a time-consuming task by using the Sleep method. In reality, this task could have been a call to a web service or a data-processing task. Occasionally, these tasks may take too long. If they do, a user may want to cancel or go in another direction. For these situations, the BackgroundWorker provides a way for you to cancel an asynchronously running task.
Canceling a BackgroundWorker task is similar to handling a change in progress. To cancel a task, you must complete a similar three-step process. The first step involves informing the BackgroundWorker that you want to support cancellation. The next step involves determining when a task should be canceled. Finally, you need to check for a cancellation while performing the background task. These three steps are shown snippet 11.16.
This snippet shows a BackgroundWorker that runs until a user clicks the Escape key. This code further expands the functionality of the evolving ProgressBar. As you can see, the first step involves instructing the BackgroundWorker to support cancellations. You accomplish this by setting the WorkerSupportsCancellation property to true . By default, this property is set to false. By setting this property to true, you can support the usage of the CancelAsync method .
The CancelAsync method can be triggered in response to a user-initiated event or from something occurring during processing. This method signals that a Background-Worker should halt execution, but this doesn’t happen automatically. Instead, you must periodically check the CancellationPending property within the DoWork event . If this property is true, you stop the processing of the asynchronous task by using a return statement. This is one of the ways that the RunWorkerCompleted event can be fired.
The RunWorkerCompleted event occurs when a BackgroundWorker is finished with the task defined within the DoWork event handler. A task is determined to be finished when one of the following three situations has occurred:
- The DoWork event handler was canceled.
- The DoWork event handler successfully completed the task.
- An exception occurred within the DoWork event handler.
Depending on which of these situations has occurred, you may want to provide a specific type of update to the user. For instance, you may want to display a specific message or fade in a distinct graphic. Regardless, you need a way to determine how the task was completed. For this reason, the RunWorkerCompleted event gives you access to this information through the RunWorkerCompletedEvent-Args parameter.
The RunWorkerCompletedEvent-Args class provides a Boolean property called Cancelled that indicates whether the task was canceled. Alternatively, you can check the Error property to see if an exception occurred during the task. If an exception hasn’t occurred, the Error property will be null. This could mean that the task was successfully completed—in which case, the Result property may contain a value. This value can be assigned from the DoWork event hander. If you don’t specify a value, it will be null, which is the case in snippet 11.17.
In this snippet, you define the event handler for the RunWorkerCompleted event . Although in this example the ProgressBar is hidden regardless of what happens, you can do a number of other things. The outcome could be based on the values exposed through the RunWorkerCompletedEvent-Args parameter. This fact is a testament to the flexibility of the BackgroundWorker.
The BackgroundWorker is a powerful component for creating a rich and responsive UI. This component is great for working with long-running tasks, which may include data processing and communicating with web services. If you run into a situation where you need to download files, there’s a better way. This alternative approach uses the WebClient class to asynchronously retrieve content on demand.
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 options discussed in chapter 6 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 options in chapter 6. 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.
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.
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 11.18.
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 . This Uri is used later as a parameter to the DownloadStringAsync method . 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.
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 kinds of files can be downloaded asynchronously using the OpenReadAsync method (snippet 11.19).
public void RequestStreamContent()
Uri uri = new Uri("http://silverlightinaction.com/video3.wmv");
WebClient webClient = new WebClient();
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 . 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.
After a download request has been sent, you need to know when the download has completed. We’ll discuss this in 11.4.3. When a larger download request is started, it can be valuable to keep the user informed as the download progresses. This involves monitoring the progress of the 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.
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 11.20.
Snippet 11.20. C#: Responding to the DownloadProgressChanged event. This event handler displays the progress of a download within an assumed TextBlock.
void webClient_DownloadProgressChanged(object sender,
StringBuilder sb = new StringBuilder();
sb.Append(e.BytesReceived + " of ");
sb.Append(e.TotalBytesToReceive + " bytes received");
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 and TotalBytesToReceive .
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 the 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 (snippet 11.21).
Snippet 11.21. C#: Canceling a download request. This snippet assumes a pretend Cancel button triggers the cancellation. In addition, this snippet assumes a WebClient variable called webClient exists.
WebClient webClient = new WebClient();
protected void cancelButton_Click(object sender, RoutedEventArgs e)
This snippet shows an event handler for an imaginary Cancel button . This event handler cancels a download by using the WebClient class’s CancelAsync method . 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 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 property 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 11.18 and 11.19. 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 what to do with the content.
When we discussed the process of requesting content in 11.4.1, 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 OpenRead-Completed 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 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 (snippet 11.22).
Snippet 11.22. C#: This snippet uses a WebClient to call a JSON web service. This web service returns the weather conditions for a latitude and longitude in JSON.
private void RequestContent()
Uri address = new Uri(
WebClient webClient = new WebClient();
webClient.DownloadStringCompleted += new
void webClient_DownloadStringCompleted(object sender,
HtmlWindow window = HtmlPage.Window;
This snippet uses a WebClient to call a web service that returns data in JSON. This web service is called through the DownloadStringAsync method . As we discussed in 11.1.1, this method asynchronously begins downloading string-related content. When this download has completed, the DownloadStringCompleted event will fire . 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 . 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 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.
As you saw in chapter 7, 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 in 11.1.1, 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 process is demonstrated in snippet 11.23.
private void RequestContent()
Uri address = new Uri("http://www.silverlightinaction.com/video2.wmv");
WebClient webClient = new WebClient();
webClient.OpenReadCompleted += new
void webClient_OpenReadCompleted(object sender,
This snippet loads a video, on demand, when the RequestContent method is called . This method requests a video, from a remote server, through the OpenReadAsync method . This method asynchronously downloads the requested video as a Stream, which becomes available as soon as the OpenReadCompleted event is fired . When this event is triggered, you can extract the downloaded Stream through the OpenReadCompletedEventArgs property, Result . In the case of snippet 11.23, this Stream represents a media file that needs to be loaded.
1 The content is downloaded asynchronously if it hasn’t been previously requested. When content is downloaded, it is stored in the browser’s cache. If the content is still there and the content is requested again, it will be retrieved from the cache instead of being downloaded.
Loading media content from a Stream involves using a method called SetSource . 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.
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 11.24.
Snippet 11.24. C#: Loading media content that has been downloaded on demand. Note that the somefont.TTF file doesn’t exist. This snippet is solely for demonstration.
private void RequestContent()
Uri address = new Uri("http://www.silverlightinaction.com/somefont.TTF");
WebClient webClient = new WebClient();
webClient.OpenReadCompleted += new
void webClient_OpenReadCompleted(object sender,
FontSource fontSource = new FontSource(e.Result);
myTextBlock.FontSource = fontSource;
myTextBlock.FontFamily = new FontFamily("webdings");
This snippet dynamically retrieves a font, then uses it with a TextBlock. The first step involves using the OpenReadAsync method to request the font ; 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 Font-Source. Thankfully, the FontSource class’s constructor takes a Stream (how convenient!), so you can load up a downloaded Stream as a FontSource . This FontSource instance can then be set to the FontSource property of either a Text-Block or TextBox. This snippet uses a TextBlock . 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.
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 11.25.
This snippet demonstrates how to extract a file out of a dynamically downloaded package. The .zip file is requested using the OpenReadAsync method . 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 11.25. For the sake of reference, the process can be outlined as the following:
- Retrieve the part to extract as a relative Uri .
- Convert the downloaded Stream, which represents a .zip file, into a StreamResourceInfo instance .
- Extract the individual part using the statically visible Application.GetResourceStream method .
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 11.25, 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 need to 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 on the fly, or you may need to download complex application logic. Both of 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.
Splitting an application into multiple application modules can make it significantly 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. By adding a module, 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 11.26.
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 . 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 . 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 . 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 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.
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 (snippet 11.27).
void webClient_OpenReadCompleted(object sender,
if ((e.Cancelled == false) && (e.Error == null))
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 .
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 11.27.
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. Another valuable runtime feature is the ability to compile and execute code on the fly. This unique attribute of Silverlight is made possible through the DLR.
DLR enables on-the-fly compilation and execution of a number of scripting languages including JScript, IronPython, and IronRuby. Each of these language implementations runs on top of the CLR. Because of this, these languages can still take advantage of features such as garbage collection, memory management, and type-safety checking. In addition, the DLR supports a plug-in model that enables additional languages to be created and used within Silverlight, so you can create your own language by creating a parser. But, we won’t be creating our own language in this section.
We do recognize that comparing the features of dynamic (for example, Iron-Python) and static (for example, C#) languages is a highly debated topic. Because of this, this section is not intended to endorse the use of one type of language, static or dynamic, over the other. Instead, this section will discuss the advantages and disadvantages of two interesting characteristics of dynamic languages. These features include the built-in support for dynamic typing and metaprogramming.
Dynamic typing means that type checking is performed when your code is executed. This approach is also known as late-binding, which means that an object’s value type is evaluated at runtime. Because of this, you have the flexibility to change the reference to an object instead of its value. To illustrate this fact, consider the static and dynamic code statements shown in table 11.4.
C# (Statically typed language)
IronPython (Dynamically typed language)
|int myValue = 8;||myValue = 16;|
|myValue = "32"; // Compile-Time Error!||myValue = 8|
|myValue = 16||myValue = "32" # No Error!|
This table shows a small example that demonstrates the typing behavior difference between static and dynamic languages. In the IronPython example, the myValue object isn’t evaluated until it’s used. This example doesn’t really explain the value of dynamic typing—for you to better understand the value, we need to explore the advantages and disadvantages swirling around this approach.
Dynamically typed languages in the DLR provide several advantages when compared to their statically typed equivalents. One advantage is the ability to dynamically redefine the reference name to which a value belongs. Another benefit of dynamically typed languages is the emphasis they place on testing.
Dynamically typed languages run on the idea that testing is more important than type-checking. This idea is based on the agreement that an application isn’t entirely complete until it passes all its requirements. These requirements generally define the intended runtime behavior of an application, so compile-time type-checking appears to only test one small requirement of an application: Will it run? Unfortunately, some developers have a tendency to behave as if an application is complete once it compiles. By holding off on type-checking and compilation until execution, testing is encouraged. This is at least one way of thinking about how dynamic typing can add value. Dynamic typing can also add value through a certain style of dynamic typing called duck typing.
Duck typing is a style of dynamic typing that provides a way to simplify the overall implementation of an application. This streamlined approach is made possible because duck typing removes the need to create base classes and interfaces. In turn, this approach can enhance the process of creating design patterns throughout an application. In order to illustrate, consider the following types defined in C# and Iron-Python (table 11.5).
C# (Statically typed language)
IronPython (Dynamically typed language)
public class Bird
Both classes defined in this table represent objects that can fly. As you’ve probably noticed, the IronPython representation is much more compact. Some will say that this version is more readable. And although readability is valuable, the real value in duck typing begins to become clear when you want to use these objects in an abstracted approach. To illustrate, consider asking a flying object to fly once a button is clicked. The code to accomplish this task is shown in table 11.6.
Table 11.6. Making the objects fly when a button is clicked. The C# version references an interface defined in the next table.
C# (Statically typed language)
IronPython (Dynamically typed language)
void Click(object sender, MouseEventArgs
def ButtonClick(sender, event):
At this point, the dynamic language implementation in this table is ready to go. The reason why is because the types aren’t checked until runtime. Because of this, the call to the Fly method will work as long as the calling object defines a method called Fly. If a method called Fly wasn’t defined in the type declaration, then a runtime error would be fired. But, in order to get the C# implementation to compile, you still have some work to do.
To get the C# implementation from table 11.6 to work, you need to implement either an interface or base class. As you may have guessed by the I prefix, we chose to use an interface. This interface, along with the class modifications, is shown in snippet 11.28.
This snippet shows the completed reworking of the flying object library. To get the code to run, you add an interface . Once the interface is created, you update the class definitions to ensure that they implement the interface . Although this isn’t difficult, it’s still time-consuming, so some will view dynamic languages as more productive and flexible. But, this flexibility doesn’t come without potential problems.
Dynamically typed languages have several drawbacks compared to their statically typed siblings. Some believe that dynamically typed languages tend to introduce more runtime errors. These runtime errors can be caused by another potential problem of dynamic typing: readability.
Method signatures used in a dynamically typed language can be difficult to read and understand. The reason why is because dynamic languages don’t require parameters to specify their types. Although this style may not cause problems in isolated applications, it can wreak havoc in integration scenarios because parameter names with ambiguous meanings can be confusing. For instance, which of the method signatures in table 11.7 do you find easier to understand?
C# (Statically Typed Language)
IronPython (Dynamically Typed Language)
public void ProcessForm(int formID)
At first glance, the method signatures in this table seem fairly easy to understand, but if developers were exposed only to the IronPython version, they may attempt to pass a string in for the formID. This value could easily be converted to an integer, which is explicitly required by the C# version, but some developers may mistakenly assume that a valid formID can contain letters. Doing this could potentially trigger untested runtime errors that could have been caught during compilation in a static language.
In general, dynamic typing provides a flexible way to write applications. This flexibility has its set of advantages and disadvantages. Keep in mind, though, that this feature is but one of several provided by dynamic languages. Another one of the interesting traits of dynamic languages is their built-in support for metaprogramming.
In addition to dynamic typing, dynamic languages expose another interesting feature known as metaprogramming. Metaprogramming is the idea of creating software that writes software. At first glance, you may think that this description sounds like code generation. In a sense, it is. But with metaprogramming, code can be created, modified, compiled, and executed completely during runtime. This includes properties, methods, and even full-blown classes. This programming technique comes with its own share of advantages and disadvantages.
The idea of writing software that writes software can be quite compelling. To put a frame around this valuable picture, consider the typical development approach, which usually involves six steps. Each is shown in figure 11.2.
This figure shows the basic workflow of writing an application. You’ve personally experienced this process hundreds of times. You know that, if you have a particularly feisty bug, the process of launching an application to test a single change can be quite time-consuming. This is especially true if you have to create a bunch of test data in the process. What if there were a way to insert new code to fix a bug while the application was running? If you could do this, your development workflow would look like figure 11.3.
The workflow shown in this figure seems like a dream. By using the capabilities of metaprogramming, you can accomplish this streamlined workflow. For instance, you could create an application that exposes a utility mode, which would expose a text box that allows you to enter some dynamic code. This code would then get sent through the DLR and dynamically update the application. The update could occur without ever having to stop the application!
As you can see, this approach can remove two of the most time-consuming tasks from the development workflow. This was accomplished by modifying and executing code during runtime. Our example is just one small use for metaprogramming. There are tons of others, and there are entire books dedicated to the subject. We designed this specific example to introduce the idea of metaprogramming at a high level and, at the same time, to introduce some of the potential disadvantages of metaprogramming.
Metaprogramming is somewhat like an extremely high-powered chainsaw. It can be an incredibly powerful tool in the right scenarios; but, if you aren’t careful, you could lose an arm. If metaprogramming is used incorrectly, it can introduce security risks.
In the example of the application with the utility mode, a major security flaw was introduced. For instance, if every user was given access to this mode, code could be inserted that would make the application inoperable. Even worse, users could modify code that would give them access to parts of the application they shouldn’t have access to; it’s incredibly critical to examine the ways your dynamic code is accessible during runtime. In fact, you probably wouldn’t want to deploy a utility mode into a production environment.
This small example shows the power and responsibility that comes with metaprogramming, one of the powerful features available within the DLR. This runtime gives you another set of tools for your application development efforts, which can be simplified by learning more about the DLR. If you’d like to learn more about the DLR, there are a number of books that discuss this topic in further detail.
Throughout this chapter, we’ve explored a number of items that can enrich a user’s experience. This experience can be enhanced using the runtime development options shown in this chapter. These options give you the ability to store and retrieve data locally. In addition, you can load XAML, process data, and download files—all without interrupting the UI. You can even go as far as modifying code during runtime with the DLR.
The features of this chapter create interesting development scenarios that can infinitely improve your applications. These improvements come in the form of small, incremental performance gains, which can transform your applications into truly enjoyable user experiences. Ultimately, the goal is to share these experiences with the world, and to achieve this goal, you need to take a number of deployment considerations into account. These deployment considerations happen to be the topic of the next chapter.