About the book
This is a sample chapter of the book
IronPython in Action. It has been published with the exclusive
permission of Manning.
Michael J. Foord and Christian Muirhead
Get 30% discount
DotNetSlacker readers can get 30% off the full print book or ebook at
www.manning.com using the promo code dns30 at checkout.
The Silverlight IronPython in the browser Series
A Silverlight Twitter client
Figure 1: An IronPython Silverlight Twitter client
This example consists of about 600 lines of code in total. We won't go through all the code line by line, but through it we can explore the following aspects of working with Silverlight:
- Cross-domain policies and tips for debugging Silverlight programs
- Creating user interfaces
- Making web requests from Silverlight
- Using XML from the Twitter API
- Threading and asynchronous callbacks and dispatching onto the UI thread
- Storing data in the browser with IsolatedStorage
In some ways a Twitter client is a difficult example for Twitter. It means working with data fetched from an external server, and we quickly run into problems with making cross-domain calls from Silverlight. We start by looking at what you can and can't do.
Listing 1 is an example client-access policy file that allows access to the whole domain and from any referring domain.
Listing 1: A clientaccesspolicy.xml file to allow cross-domain calls into a website
Of course, this is most useful when the domain you want to access is under your control. If it isn't your server, the typical solution is to proxy the service you want to access. This means that your application queries a server that is under your control, and the server makes the query and returns the result. Unfortunately, although Twitter exposes a nice and simple API for clients to use, it doesn't have the necessary client-access policy allowing us to query it from Silverlight. I really wanted to write a Twitter client though, because I've been having a lot of fun with it recently. You can follow me at http://www.twitter.com/voidspace.
To make the client work, we've implemented a simple Python proxy server that runs locally. Because we'll be using Chiron, which serves on port 2060, the local proxy server runs on port 9981. You can start it by executing the following command from the directory containing the example code for this section:
As far as Silverlight is concerned, calls to this server are still cross domain, so our proxy server has to provide a valid client access policy when /clientaccesspolicy.xml is requested. Before we look at the substance of the application, let's share a tip for debugging Silverlight applications.
Debugging Silverlight applications
The normal first resort for tracking down bugs in Python programs is the judicious use of print statements. This technique doesn't work in Silverlight, and to make it worse, the error messages can be extremely cryptic. This was to save space in the Silverlight runtime and may be improved in a future version of Silverlight.
There's a way around this, though. Python allows us to override standard output with a custom writer. Listing 2 shows how to create a custom writer and set it on
sys.stdout. The actual code in the example is slightly different. We shouldn't modify the browser DOM from anything other than the UI thread, so it contains code to dispatch the write in case we want to print from an asynchronous callback off the main thread. This topic is covered in more detail later in the chapter.
Listing 2: Diverting standard out to an HTML text area
print statement is executed, the
write method of our custom writer is called. This looks up the HTML element with the id
debugging and sets the text on it. We fetch this element from the HtmlDocument (see http://msdn.microsoft.com/en-us/library/system.windows.browser.htmldocument(VS.95).aspx.), which we get hold of via System.Windows.Browser.HtmlPage.Document. Figure 2 shows the
textarea with the debugging output from running the Twitter client
Figure 2: The HTML textarea containing the output of print statements from the Twitter client
Silverlight provides other ways to interact with the browser DOM, which we look at shortly. First we look at parts of the Silverlight UI model that we haven't already seen.
The user interface
The main user interface for the Twitter client is a Border containing a StackPanel, two classes we worked with in the chapter on WPF. The main application is contained in a class called MainPanel that inherits from StackPanel. The rest of the user interface is laid out by nesting StackPanels in new borders where necessary. From the wonderful color scheme I chose, you should be able to see the different elements nested inside each other. When you first log in, you're presented with the login panel shown in figure 3.
Figure 3: The Silverlight Twitter client login panel
This is a horizontally oriented StackPanel (in a border) with nested vertical StackPanels containing the username/password textboxes, the Login button, a check box, and a textblock for messages.
We haven't yet used the CheckBox, but like the other user-interface components, it's simple. Listing 3 shows the configuration of the Login button and the Remember check box in the Login panel.
Listing 3: Configuring the Button and CheckBox in the Login panel
The textbox for the username is a straightforward TextBox, but we can't use that for entering the password because we don't want it to be visible while it's being typed. Thankfully, a password textbox is one of the standard controls. The difference between the PasswordBox API and a straight TextBox is that instead of setting and fetching the Text property, we use the
Password property. Once the user has logged in, the tweets are fetched from the Twitter API and displayed inside a grid.
The Grid, the ScrollViewer and the Hyperlink button
The grid is inside a ScrollViewer, so that all the Twitter messages can be seen. The grid has two columns. The Twitter usernames are displayed in the left column as clickable links, with the Twitter messages in the right column (these are all visible in figure 1). The code that does all this is in listing 4.
Listing 4: Creating and populating the main grid for the Twitter client
Several details are worth noticing in this code. Using color brushes in Silverlight is slightly different from in WPF. Instead of using something like Brushes.White to set the background, we construct a brush with SolidColorBrush(Colors.White). The HyperlinkButton is placed at the left and the center (vertically) by setting horizontal and vertical alignment. By default the HyperlinkButton opens in the same window.
You can specify that the link should open in a new window by setting
HyperlinkButton.TargetName = "_blank". Although currently that stops the link button working altogether on Safari on the Mac!
Because the Twitter messages are often longer than the width of the grid, we need to ensure that the TextBlock wraps the text by setting the
TextWrapping property. We've covered the important parts of the user interface, so now we look at how we access server resources from Silverlight.
Accessing network resources
The basic class for accessing network resources from Silverlight is the WebClient, which can be used for both GET and POST requests. It has an asynchronous API, so you configure it to fetch an API and provide callbacks to handle the response.
The Twitter API is extremely easy to work with (see http://groups.google.com/group/twitter-development-talk/web/api-documentation. I got this message when I entered this URL: The Twitter API documentation has moved to http://apiwiki.twitter.com) by virtue of its XML or JSON over REST interface.
To verify login details or fetch the latest messages for a Twitter user, you fetch a URL from the Twitter API (using basic authentication with the user credentials), which then returns either XML or JSON, depending on which format you asked for in the URL. Because of the cross-domain policy, we can't access the Twitter domain, so we can leave the proxy server to handle the authentication. The
twitter_proxy module wraps the WebClient in a Fetcher class, shown in listing 12, which takes a callback function along with the username, the password, and the action to be performed.
Listing 5: The Fetcher class for downloading web resources with WebClient
It's called like this:
If you watch the local proxy for the first time this is called, you'll see that before fetching the requested URL, Silverlight asks for /clientaccesspolicy.xml. If the server doesn't respond to this, then an exception will be raised.
WebClient and changing data
Under the hood, WebClient caches requests for us. If we fetch the same URL later, even from a new instance, the same data will be returned instead of a new request being made. This is a problem if the data you want changes.
In the Silverlight Twitter client we solved this by adding a digit to the end of the URL sent to the proxy. The digit is incremented with every request so that WebClient sees a different URL every time.
Because we're putting the action, username, and password as parameters into the
GET string, they need to be URL encoded. This is the task of the HttpUtility class, which also has a corresponding
UrlDecode method that could be useful if you ever want to look at the query parameters of the current UThis is made available through System.Windows.Browser.HtmlPage.Document.DocumentUri.RL.
When the response is available, the DownloadStringCompleted event fires and the completed method is called. If the request completes successfully, then our original callback is called with the response as a string.
Parsing the XML from Twitter
The main client application takes the results (a string of XML) and needs to parse this into messages suitable for populating the grid. Because Silverlight comes with the XmlReader (and the associated classes and enumerations), we can reuse the XmlDocumentReader class from chapter 5 to parse the response.
The only changes we needed to make to this class were to add a reference to the System.Xml assembly and the addition of a single line to handle the XML declaration that we didn't need for MultiDoc. Since we have our XML as a string, we can pass a TextReader into the call to XmlReader.Create instead of a stream. Just as in the desktop .NET framework, XmlReader lives in the System.Xml assembly, so we need to add a reference to it before we can import from it.
The XML that Twitter returns when we ask for a user's messages has a top-level statuses element containing a nested series of status blocks. Each one of these has various elements that describe the message and the user who posted it. For our basic client, we're interested in only the body of the message itself and the name of the user who posted it. With the event-based parsing of XmlReader, we need only define handler methods for the parts of the Twitter XML that we're using, ignoring the rest. You can see the (brief) code that does this in the twitter module. It returns a list of dictionaries, each dictionary with name and text members, ready for displaying in the grid.
Listing 6: Making POST requests with HttpWebRequest
This Poster class wraps up the API, so that when we instantiate Poster with the data for the post and a callback function, it'll make the request, and the callback will be called with the results once the request is complete.
Posting to the proxy
Since the only time we make POST requests to the proxy server is to post a new message, there's no need to specify the action in the URL. When I originally implemented this, I set the post URL to be http://localhost:9981 and then spent a long time trying to find out why it didn't work.
The client access policy specifies that all URLs below / are allowed, so changing the post URL to http://localhost:9981/ (with the trailing slash) worked!
As is common in .NET, asynchronous callbacks often happen on a different thread than the one you create them from, and we need to handle this.
Threads and dispatching onto the UI thread
Like WPF and Windows Forms, the Silverlight user interface runs within a single main thread. Any operations that interact with user-interface elements, or access the browser DOM, should be done from this thread.
Fortunately, this is straightforward. WPF uses dispatchers to invoke delegates onto the UI thread, and Silverlight includes a cut-down version of this system. Silverlight provides a single dispatcher, which is accessible via the Dispatcher property on user-interface components. From IronPython, functions that we pass into the
Dispatcher.BeginInvoke method are invoked onto the main thread.
A lot of the core threading classes from .NET are available in Silverlight. Listing 14 creates a new thread, and after a brief pause it uses the dispatcher to invoke a function that changes the text on a textblock.
Listing 7: Using the dispatcher to modify the user interface from another thread
When this code is run, the textblock displays the message "Created on thread 1." After three seconds, this changes to show the thread ID of the new thread that we created. This is simple enough, but there might not always be a convenient user interface element available on which to dispatch from code that needs it. The Twitter client has a dispatcher module that does this for us. When the UI is first created, the main application (in app.py) calls the function
SetDispatcher. The dispatcher module also exports two other functions, Dispatch and
GetDispatchFunction. These either immediately dispatch a function for us or turn a function into one that's dispatched when it's called (which is useful for creating dispatched callbacks). Dispatch and GetDispatchFunction are used throughout the Twitter client, where code might interact with the user interface.
A common need is to have some event regularly occur at a timed interval. We use this in the Twitter client to fetch the latest tweets every 60 seconds. We can do this with the DispatcherTimer. The following listing uses a DispatcherTimer to update a counter in a textblock.
Listing 8: Using a DispatcherTimer for timed events on the UI thread
When this code is executed, the Tick event fires every two seconds and increments the counter. Because Tick is executed on the UI thread, there's no need for us to explicitly dispatch the callback. If we didn't need to interact with the user interface, then we could use the System.Threading.Timer class instead.
Another of the Silverlight APIs that the Twitter client uses is the isolated storage system, for storing user login details in the browser.
IsolatedStorage in the browser
Isolated storage provides a mechanism for applications to store data in the browser cache. The intent of this is to provide a temporary cache for applications or for storing configuration information. Data stored there does persist but is destroyed if the user clears the browser cache. With this in mind, the default limit per application is 100 kilobytes of storage. You can request more, which will present the user with a dialog box requesting permission to increase the limit for this application.
Isolated storage provides a filesystem-like mechanism that we access through the System.IO.IsolatedStorage namespace. We can list the files (or directories) stored and load, save, or delete files.
In the Twitter client this is used for the user login credentials. If the Remember check box is checked when the user logs in, then the username and password are saved in a file. In a plain text file, but this isn't recommended for production systems storing sensitive user data such as passwords!
When the application is first loaded, if that file exists in the application storage, then the file is loaded and the username and password textboxes are populated from it. The basis of using isolated storage from our applications is to get a data store by calling
IsolatedStorageFile.GetUserStoreForApplication(). This returns the IsolatedStorageFile instance for the current application. The next listing shows three functions to load and save files listed in the data store and to list the contents.
Listing 9: Using the Silverlight isolated storage
The data store isn't a flat file system; we can create subdirectories and work with those as well. The store has many useful methods; these are listed at http://msdn.microsoft.com/en-us/library/system.io.isolatedstorage.isolatedstoragefile_members(VS.95).aspx.
One of the more important ones is
TryIncreaseQuotaTo to request an increase in the amount of storage available. This method can be called only from inside the event handler of a control such as a button. You pass in the amount of space you want (in bytes), which presents a dialog box to the user to approve the request. The method returns a Boolean indicating whether the request succeeded or not. The following snippet of code shows a request to double the amount of storage for an application:
The next figure shows the dialog box presented to a user when this code runs on Safari under Mac OS X.
Figure 4: Requesting to increase the storage for a Silverlight application
If you attempt to execute this code outside a user-interface event handler, then it will fail and return False without showing the dialog box.
We've now seen that Silverlight contains all the necessary ingredients for creating serious applications that live on the web. Writing web applications is different from programming for the desktop. Although the CoreCLR lessens that difference, the key to creating effective applications is understanding the difference, and that means being able to make good use of the important Silverlight APIs such as IsolatedStorage.
We've only skirted around the edges of a couple more important Silverlight APIs: working with videos and the browser DOM.
Videos and the browser DOM
Despite its diminutive download, Silverlight packs in a great deal. The intended use for Silverlight spans the gamut of games, applications, and media streaming, so along with the .NET Base Class Libraries it comes with APIs specific to these tasks. Some of these are the adapted version of their complementary WPF class in the desktop framework. Others, like those for working with the browser DOM, are new to Silverlight. In this final section of the chapter, we use two more of the core Silverlight APIs.
The MediaElement video player
The media capabilities are provided in part through the MediaElement class. The figure below shows a video as part of a Silverlight canvas.
Figure 5: The MediaElement class in action
This class is a control, and like the other controls, it lives in the System.Windows.Controls namespace. Having instantiated it, you specify a data source as a Uri, as in the following listing.
Listing 10: Using the MediaElement video control
As always, the MediaElement has methods, properties, and events that we haven't used here. The methods to start and stop playing are Play and Pause, but the video will start playing as soon as it has downloaded events, so is the Play method is unnecessary. Other useful properties are Position, which we can set with a TimeSpan object, and both Width and Height to scale the video. As with setting the
Height scales, setting one automatically adjusts the other.
In addition to using it directly, we can use the MediaElement as the source for a VideoBrush, which we can use as the foreground mask on another control or to fill a shape, which we then transform or animate. The listing below shows how to set a video as the foreground mask for the text in a textblock.
Listing 11: Setting a VideoBrush with a video on a TextBlock
MediaElement exposes an event called MediaEnded that fires when the video ends. We make the video loop by hooking up a restart function to this event that restarts the video. The next figure shows the results of setting a VideoBrush on a TextBlock from IronPython.
An important aspect of Silverlight that we've touched on only briefly is working with the browser and the Document Object Model.
Accessing the browser DOM
We flirted with the DOM when we looked at diverting standard output, so that debugging print statements would appear in an HTML text area. We access HTML elements in the page through the
Document property on System.Windows.Browser.HtmlPage. We can access elements by using their id as the attribute name on Document, which does a dynamic lookup (one of the joys of working with a dynamic language). This returns an element object (specifically, an HtmlElement object. See http://msdn.microsoft.com/en-us/library/system.windows.browser.htmlelement(VS.95).aspx), with which we can do many useful things. The next listing shows how to set the
innerHTML on an element, using
GetElementById as an alternative way to fetch elements, modifying style rules on an element with
SetStyleAttribute, plus setting properties with the
Listing 12: Interacting with DOM elements from inside Silverlight
As well as using
innerHTML we could also use innerText. Because we have access to these attributes, we can use all our favorite AJAX tricks from Silverlight. Some properties on HTML elements require us to use the
SetProperty methods rather than simple attribute access. Fetching the
The next listing does three things:
Adds a mythical DoSomething Python function as an event handler for a button in the HTML with the id some_button.
The SilverlightHost object itself isn't particularly interesting, but it exposes a Content subobject that is. This has members (see http://msdn.microsoft.com/en-us/library/system.windows.interop.content_members(VS.95).aspx) like ActualHeight and ActualWidth that tell us the real size of the Silverlight control within the web page.
IsFullScreen will not only tell us if the Silverlight control is operating in full-screen mode, but if set to
True from a button event handler it'll also switch the plug-in to full-screen mode. Most important, though, the content object also has a
ReSized event, which fires when the control changes size. If, instead of creating the Silverlight control with a fixed size in the HTML embedding code, we let the browser size it, then we can respond appropriately (perhaps re–lay out the UI) when the control is resized.
We've had only one chapter to learn about Silverlight, but it's clear that this framework has a huge amount of potential. It's particularly exciting that programming it from IronPython is such a good experience; long may the reign of dynamic languages on the web continue.
Rich internet applications are already an important part of the internet revolution, and they're only becoming more important. Silverlight is one of the new frameworks that allow programmers to really take advantage of client-side processing power in web applications. One of the tools we used when writing this chapter was the Silverlight IronPython Web IDE (this is available online at http://www.voidspace.org.uk/ironpython/webide/webide.html). This allows us to experiment with the Silverlight APIs by executing code in the browser, and it has several examples preloaded, including some topics we didn't have space for here.
There's a lot we haven't had time to use. We haven't looked at loading XAML or initializing controls from XAML or animations with the StoryBoard. Because the Silverlight user-interface model is based on WPF, using these features is very similar to using them from WPF, and this includes using Expression Blend (to use Expression Blend with Silverlight you'll need version 2.5 or later) as a design tool. With clever structuring, your desktop and online versions of your applications could share a lot of their code and XAML.
This is one of the best things about working with Silverlight; much of our existing knowledge about .NET and IronPython is directly applicable.
Get 30% discount
DotNetSlacker readers can get 30% off the full print book or ebook at
www.manning.com using the promo code dns30 at checkout.
Manning Publication publishes computer books for professionals--programmers, system administrators, designers, architects, managers and others. Our focus is on computing titles at professional levels. We care about the quality of our books. We work with our authors to coax out of them the best writi...
This author has published 33 articles on DotNetSlackers. View other articles or the complete profile here.
Please login to rate or to leave a comment.