Silverlight Page Turning Made Simple
Code download available at:WickedCode2008_05.exe(1110 KB)
The PageTurnDemo Application
Using the Page-Turn Framework
Two years ago, I was cruising down a hallway in Redmond when a friend stopped me. "I have something you need to see," he said. He popped open his laptop and showed me a demo that changed my life. That demo was an early version of the SilverlightTM Page Turn Sample, which you can see now at silverlight.net/samples/1.0/Page-Turn/default.html.
I couldn't believe what I was seeing because the demo was running—gasp—in a browser! More surprisingly, it didn't require the Microsoft® .NET Framework, it didn't require Internet Explorer®, and although no one knew it at the time, one day it wouldn't even require Windows®.
PageTurn is the quintessential Silverlight 1.0 demo. I use it all the time when I want to impress upon first-timers what Silverlight is all about. But look inside PageTurn and you'll find that building a page-turning application of your own is no small chore. PageTurn relies on transforms, clipping regions, dynamically created XAML objects, and more; the source code requires time and effort (and no small knowledge of Silverlight) to understand. It deftly demonstrates some of the most prolific capabilities of Silverlight, but it was not necessarily designed for general reuse.
The PageTurnDemo Application
Before I introduce the framework, let's examine an application built around it. The PageTurnDemo application pictured in Figure 1 lets you page through the first several pages of the November 1988 issue of Microsoft Systems Journal, now known as MSDN® Magazine. (How do you avoid copyright issues when reproducing pages from a magazine? Use pages from the magazine you work with, and use an article you wrote yourself, no less!) Each of the pages is a scanned image, but one of them—the final page—overlays the image with Extensible Application Markup Language (XAML) text. I included the text to reinforce a key point: pages fed to the framework aren't limited to just images; with few constraints, they can contain arbitrary XAML, including Images, TextBlocks, MediaElements, and more.
Figure 1** PageTurnDemo Shows a Partially Turned Page **
You can run the demo by downloading the source code and launching it from Visual Studio® 2008, or you can view it at wintellect.com/CS/blogs/jprosise/archive/2008/10/29/page-turn-framework-updated-for-silverlight-2.aspx. Once the magazine cover appears (a progress bar apprises you of the progress of the download that retrieves all the images used in the app), use the left mouse button to drag the mouse over the cover from right to left to page forward. You can continue dragging left over right-hand pages to reveal more pages, or drag from left to right over left-hand pages to page backward.
Using the Page-Turn Framework
PageTurnDemo demonstrates the four basic steps you'll need to complete to use the page-turn framework. The first step is to include PageTurn.js—the script file containing the framework implementation—in the HTML file. Here's the relevant line in Default.html:
<script ... src="PageTurn.js"></script>
_ptf = new PageTurnFramework(_control, _control.content.findName('PageTurnCanvas'));
The first parameter passed to the PageTurnFramework constructor is a reference to the Silverlight control. The second parameter is a reference to the canvas containing your pages. In PageTurnDemo's XAML document, that canvas is named "PageTurnCanvas."
The third step is to register your pages with the framework. Each page is represented by a canvas, and PageTurnFramework exposes an addPage method that you can call to register pages. AddPage accepts two parameters:
The first parameter is a reference to the canvas representing the left-hand page in a pair of facing pages; the second is a reference to the canvas representing the right-hand page. You can call addPage as many times as you need to register all your page pairs. And then, the final call to addPage must be followed by a call to initializeFramework:
The initializeFramework method initializes the internal state of the framework. Among other things, it creates several XAML objects used to clip and rotate pages, and it also registers handlers for key events.
The fourth and final requirement for using the framework is to be sure to clean up properly. To avoid memory leaks, browser-based apps that programmatically register event handlers should unregister them as well. PageTurnFramework includes a dispose method that deregisters the event handlers registered by addPage and initializeFramework. In PageTurnDemo, an onunload attribute in the <body> element calls a local dispose function when the page unloads:
<body ... onunload="dispose()">
This local dispose function, which lives in Default.html.js, calls the framework's dispose method:
if (_ptf != null) _ptf.dispose();
I used a DOM event to trigger calls to dispose because Silverlight doesn't fire unload events. You could just as easily use window.unload events if you'd prefer.
The PageTurnFramework class exposes six public methods that you can call to add page-turning functionality to your apps (see Figure 2). PageTurnDemo uses three of them: addPage, initializeFramework, and dispose. The other methods can be used to add extra functionality. For example, if you wanted to include a navigation bar consisting of page thumbnails in your app (as the Silverlight PageTurn demo does), you could call PageTurnFrame work.goToPage when a thumbnail is clicked to navigate directly to the corresponding page.
Figure 2 Page-TurnFramework API
|addPage||Registers a page pair with the framework.|
|Dispose||Releases resources held by the framework for proper cleanup.|
|getCurrentPageIndex||Returns the 0-based index of the page pair currently displayed.|
|getPageCount||Returns the number of page pairs added with addPage.|
|goToPage||Displays the specified page pair.|
|initializeFramework||Initializes the page-turn framework.|
The page-turn framework imposes a few basic requirements on the structure of XAML documents. Those requirements are:
- Represent each page in a page pair with a XAML canvas (a "page canvas").
- Include a "page-turn" canvas that is a container for all of the page canvases.
- Assign a width, height, and background color (even if it's Transparent) to the root canvas.
The reason for the third requirement is that at initialization, the framework registers a handler for MouseLeave events fired by the root canvas. These events are used to complete page turns if the cursor leaves the Silverlight control with a page partially turned.
As a matter of best practice, all Silverlight 1.0 apps that capture the mouse, as the page-turn framework does when a turn begins, should process MouseLeave events emanating from the root canvas and take that opportunity to release the mouse. And in order for an element to receive mouse events such as MouseEnter and MouseLeave, the element must be "hit testable" within Silverlight. This is done by ensuring that the element (in this case, a Canvas) has a size and that it has a "non null" background.
With these requirements in mind, Figure 3 shows the general structure of a XAML document used with the page-turn framework. The page-turn canvas is the one you pass to PageTurnFramework's class constructor. Page canvases are passed to PageTurnFramework.addPage. The page-turn canvas and the page canvases should generally be tagged with explicit widths and heights in order to ensure that the mouse events used by the framework fire properly no matter what kind of XAML content the canvases contain. The rest is just XAML.
Figure 3 XAML Structure
<Canvas xmlns="http://schemas.microsoft.com/client/2007" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Background="color" Width="width" Height="height"> <!-- Other XAML content goes here --> <!-- Page turn canvas --> <Canvas> <!-- Canvases representing first page pair --> <Canvas> <!-- Content for left-hand page goes here --> </Canvas> <Canvas> <!-- Content for right-hand page goes here --> </Canvas> <!-- Canvases representing additional page pairs go here --> </Canvas> <!-- Other XAML content goes here --> </Canvas>
The page-turn canvas can happily exist alongside any other rich content in your XAML document. And individual pages can consist of simple XAML images or complex XAML renderings. You should generally avoid using the Clip property of the page canvases because the framework itself uses that property. However, you could always declare a subcanvas inside a page canvas and use the subcanvas's Clip property to define clipping regions.
If you want the first left-hand page to be blank so that the first right-hand page looks like the cover of an unopened book, simply declare an opaque rectangle in the first left-hand page. Similarly, you can use an opaque rectangle for the final right-hand page to create the appearance of a closed book when the user turns to the last page. PageTurnDemo does both to perpetuate the illusion that you're flipping through the pages of a magazine.
When you scan pages from a book or magazine to use with the page-turn framework, as I did for PageTurnDemo, the scans often have shadows at the inside edges, which give the pages a more realistic appearance. If you don't use scanned images, a bit of XAML can simulate shadows. For the application pictured in Figure 4 (which depicts some of my radio-controlled airplanes and jets), I used the XAML rectangles in Figure 5 to create shadows around the vertical divide at the center of each page pair. Alpha values in the GradientStop colors cause the rectangles to fade from right to left on left-hand pages and from left to right on right-hand pages.
Figure 5 XAML for Creating Shadows on Page Interiors
<!-- Shadow on left-hand page --> <Rectangle Canvas.Left="380" Canvas.Top="0" Width="20" Height="600"> <Rectangle.Fill> <LinearGradientBrush StartPoint="0,0" EndPoint="1,0"> <GradientStop Color="#00000000" Offset="0.0" /> <GradientStop Color="#40000000" Offset="1.0" /> </LinearGradientBrush> </Rectangle.Fill> </Rectangle> <!-- Shadow on right-hand page --> <Rectangle Canvas.Left="0" Canvas.Top="0" Width="20" Height="600"> <Rectangle.Fill> <LinearGradientBrush StartPoint="0,0" EndPoint="1,0"> <GradientStop Color="#40000000" Offset="0.0" /> <GradientStop Color="#00000000" Offset="1.0" /> </LinearGradientBrush> </Rectangle.Fill> </Rectangle>
Figure 4** Page-Turn Sample with XAML Shadows on Page Interiors **
If any of this is unclear, you can start an application by building from PageTurnDemo. Simply replace my XAML for individual pages with XAML of your own. My page canvases are dimensioned with a width and height of 400 and 544, but you can change those dimensions to whatever you prefer.
The class constructor defines all the instance variables—the equivalent of fields in C#—needed to store internal state. For example, the statement
this._control = control;
declares a "field" named _control and initializes it with the Silverlight control reference passed to the constructor. In all, approximately 40 fields are declared and used to hold everything from the percent-complete figure for an in-progress page turn (_percent) to tokens representing registered event handlers (which dispose uses to de-register the handlers). Fields also store references to several XAML objects that are created dynamically in initializeFramework and to XAML objects registered with calls to addPage.
PageTurnFramework.prototype contains all the PageTurnFramework methods. The methods fall into three broad categories: public methods such as addPage and initializeFramework; event handlers, which act in response to the mouse events and Storyboard.Completed events that the framework uses internally; and private methods, which are used internally by the framework but aren't intended to be called from the outside. There's plenty of interesting code, but due to space constraints, you'll have to download PageTurn.js to see them.
One of the interesting aspects of the framework architecture is how it completes page turns if the mouse button is released (or the cursor leaves the control) while a page turn is ongoing. At initialization time, the framework uses createFromXaml to create a Storyboard object to use as a timer. Then it adds the Storyboard to the page-turn canvas (this is one of the reasons you need to pass a reference to the page-turn canvas to the class constructor) and registers a handler for Storyboard.Completed events.
To finish an incomplete page turn, the framework calls Storyboard.begin to start the timer. On each timer tick, it advances the page another increment (using the step size stored in the _step field) and calls Storyboard.begin again if the page turn still isn't complete. You can see this in action by turning a page halfway and then releasing the mouse button. Depending on how far the page was turned when you released it, it will move back to the fully closed or fully opened position.
The Storyboard's XAML definition is stored in the variable named _sb in the initializeFramework method. You may wonder why that definition includes an x:Name attribute whose value is a GUID. In Silverlight 1.0, Storyboards created with createFromXaml must be named or createFromXaml will fail. I had to give the Storyboard a name, but I wanted to make sure the name didn't conflict with other Storyboards used in the application. Therefore, I named it after a GUID.
Another interesting part of the architecture is how the framework uses mouse events. For canvases representing pages, addPage registers handlers for MouseLeftButtonDown, MouseMove, and MouseLeftButtonUp events. A left turn begins when a right-hand page is clicked and progresses as the mouse moves to the left with the left button held down. Similarly, a right turn begins when a left-hand canvas is clicked and progresses as the mouse moves to the right. The key method used by the event handlers is _turnTo, which advances a partially turned page to the position denoted by the percent-complete parameter.
A final aspect of the framework that you may care to examine more closely is how it uses transforms and clipping regions to depict page turns. initializeFramework creates two PathGeometry objects: one to serve as a clipping region for right-hand pages and another for left-hand pages. It also creates a TransformGroup object containing a RotateTransform and a TranslateTransform.
Figure 6 illustrates how the clipping regions and transforms are used to depict a partially turned page. The red triangle is the clipping region used on the right-hand page that's on top. As the mouse moves to the left, the clipping region grows smaller so that less of the top page is visible and more of the page underneath is revealed. The blue triangle represents the clipping region used on the left-hand page that is revealed as the turn progresses, and the yellow rectangle represents the portion of that page that is clipped out.
Figure 6** Clipping Regions and Transforms Used to Depict Page Turns **
The RotateTransform and TranslateTransform are combined in order to position and orient the page. (Yes, there is a lot of trigonometry involved!) As the mouse moves to the left, imagine the yellow rectangle sliding to the left and rotating into an upright position. Couple that with a mental image of the blue triangle increasing in size while the red one grows progressively narrower and you'll have a pretty good idea of how page turns work.
Before the ink was dry on this column, I thought of other useful additions to the PageTurnFramework API. For example, it might be useful if the framework fired an event every time a page turns so you could write handlers that update other content on the page. And it could be useful to expose properties to control key page-turn parameters, such as the width of the shadow that follows turning pages and the step size used to animate incomplete turns.
Feel free to use this framework in your own projects and to modify it. Let me know your feedback, and if you think of useful additions to the feature set and API, let me know about that, too. I'll fold in your suggestions with my own and make sure that version 2.0 of the page-turn framework is even better than version 1.0.
Send your questions and comments for Jeff to email@example.com.
Jeff Prosise is a contributing editor to MSDN Magazine and the author of several books, including Programming Microsoft .NET. He's also cofounder of Wintellect (www.wintellect.com), a software consulting and education firm that specializes in the .NET Framework. Have a comment about this column? Contact Jeff at firstname.lastname@example.org.