Windows 8 Dependency Property Strangeness
December 5, 2011
New York, N.Y.
If this blog entry popped up in a search engine because you're having problems with defining and/or animating dependency properties in the Windows 8 developer's pre-release, you might want to jump towards the end. The early paragraphs merely discuss some deadly boring Windows 8 program I've been developing.
When I began work on an EPUB viewer for Windows 8 at the beginning of November, I thought I'd be working on it all month for 30 consecutive 12-hour work days. But that didn't quite pan out. Work days are never as long as one would like (due primarily to exhaustion, I think), plus I had to interrupt work for a consulting job I'm involved in and for my monthly column in MSDN Magazine
Consequently, I haven't gotten far at all. The current version of The New Epublic pulls the HTML files out of the EPUB package and parses them, but it doesn't yet touch the CSS files. Without these CSS files, the program can't format the text content of the book. Despite the lack of formatting information, the program tries to display pages anyway:
(Reproduced at half size.) You can page forward and backward through the book and actually read pages until the program crashes. My strategy is to focus first on rudimentary page navigation before I tackle the formatting details.
Parsing the HTML files is the responsibility of a class named HtmlFile, and it uses XmlReader for this job. The enormous convenience of XmlReader is possible only because these HTML files are actually XHTML files, and hence follow strict XML syntax rules.
The program stores the entire element tree of the HTML file in nested objects of type HtmlElement. HtmlElement defines a property named Parent, a collection named Children, and properties named NextSibling and PreviousSibling to allow easy navigation through the tree. The type of the element is indicated by a property named ElementType of type HtmlElementType, an enumeration that has members named p, div, img, and so forth. A special member named _text is for storing text content, which for EPUBs is basically the entire text of the book. HtmlElement also has properties named ID, Class, Title, and Style for the most common element attributes id, class, title, and style. Other attributes are stored in a Dictionary<string, string> object.
EPUB is not a fixed-page format like PDF and XPS. It's a flow-document format intended to accomodate a variety of page sizes. From my experience in creating a plain-text ebook reader called Phree Book Reader for Windows Phone (chronicled in 7 articles in MSDN Magazine; you can find the links here), I know how important it is to avoid situations where the program needs to paginate multiple pages. You definitely don't want to paginate the entire book when it's first loaded, but sometimes you can't avoid the necessity of paginating a bunch of consecutive pages just to display one page. If the user wants to jump to the last page of the book to see who actually committed the murder, you need to allow that, and with any luck you won't keep the user in suspense too long.
An EPUB can contain (and usually does contain) multiple HTML files, and each HTML file is assumed to begin on a new page. That helps the pagination process. To jump to the last page of the book requies paginating only the last HTML file. Also, some elements in the HTML file might have the CSS property page-break-before set to indicate a new page. In an EPUB these page breaks usually correspond to the beginnings of chapters. This also helps alleviate the pagination problem. (Since the program isn't looking at the CSS files yet, it can't yet take advantage of these breaks.)
It helps for an ebook reader to cache pagination information — not necessarily the entire page layout but at least an indication where the page begins. In support of this, I defined two methods in HtmlFile named GetElementIndices and FindElement. For any particular element, the first method returns a IList<int> that uniquely defines that element's location in the element tree. An empty list is the root element (html), a list consisting of just { 0 } means the first element within html, which is head. Just { 1 } means the second element within html or body. A list of { 1 0 } means the first element within body, { 1 1 } means the second element within body, { 1 0 0 } is the first child of the first element in body, and so forth. A paginated page always begins at a particular element and (if that element happens to be _text) at a particular index within that text. These two items are part of a class named PageStart.
It's not just jumping around within the pages of a book that necessitates paginating a bunch of consecutive pages. Sometimes an ebook reader allows orientation changes between portrait and landscape. An ebook reader should also have some kind of facility to increase or decrease the text size, perhaps defined as a "zoom" factor. Although some commercial EPUBs contain actual font information, many free ones do not, and if the EPUB does not specify a font, the program should allow the user to choose a font family and font size. If the user happens to be on the last page of a chapter when making a change like this, then the program needs to paginate the entire chapter just to redisplay the last page.
Here's where it gets a bit messy, because when a chapter is repaginated for a different zoom factory, the page that the user is currently looking at will begin at a different spot in the text! What you want to do is display a newly paginated page that contains the word that appeared at the beginning of the previously paginated page. For this purpose, a CharacterOffset property appears in several classes, including PageStart. This offset indicates the number of characters of displayable text from the beginning of the HTML file.
The PageParams class defines a page size (width and height), a zoom factor (not yet implemented), and a default font family and font size. Anything that can affect pagination should be in this class. The basic page layout logic occurs in the GetPageLayout method of HtmlPageBuilder. The arguments are PageStart and PageParams objects, and a PageStart object for the next page is returned through an out argument. GetPageLayout returns an object of type PageLayout. (More on this PageLayout object shortly.)
The GetPageLayout method of HtmlPageBuilder is called by an identically named method in HtmlFile, and that method is called from the PageProvider class. A single PageProvider object accomodates an entire book. It is this class that is reponsible for caching the PageStart object for pages that have already been paginated. It is currently storing this information in this field:
Dictionary<PageParams, List<PageStart>>[] pageStarts
Notice the brackets indicating an array. The size of this array corresponds to the number of HTML files that comprise the book. I will be adding a second jagged array dimension for the page breaks within that file, which (as I mentioned) commonly correspond with chapters. The List stores the consecutive PageStart objects for pages within that chapter, and the dictionary key is a PageParams object, so multiple page dimensions and zoom factors can be maintained. For example, suppose the user has been reading a chapter in portrait mode and then turns the reader sideways or sets a new zoom factor. That chapter needs to be repaginated, but if the user switches back to the original orientation or zoom factor, the pagination information for the old configuration will have been maintained. (This information is not saved when the program is terminated, but it could be.)
Because it is responsible for caching pagination information, PageProvider defines a somewhat different GetPage method. This one has arguments of type PageRequest and PageParams. The PageRequest object can define several different types of requests — based on a particular file, chapter, and page index, or a particular character offset, or (not yet implemented) an ID string to navigage to a particular element, which is probably either the beginning of a chapter or an internal link. PageProvider also returns a PageLayout object.
The GetPage method of PageProvider is called from PageViewer, which is the control responsible for displaying pages to the user, and for processing touch input to let the user navigate among the pages.
This PageViewer control will be somewhat different for different platforms, mostly because every platform implements touch in a different way. I'm basing the PageViewer in this Windows 8 program on the PageViewer control in Phree Book Reader, but with all the gesture processing changed to Manipulation events. (I'm ignoring pinch zooming of pages and text selection for early versions of the program.)
I've been mentioning the PageLayout object generated by the page layout logic in HtmlPageBuilder. It is my intention that most of the code for processing EPUB files will be platform-independent, so PageLayout is a platform-independent collection of objects that might appear on a page. It is the responsibility of PageViewer to translate this PageLayout object into something appropriate for the particular platform — in this case a bunch of TextBlock objects on a Canvas.
Like the earlier PageViewer control in Phree Book Reader, the Windows 8 version has a "plug in" model for implementing various types of transitions between the pages of the book. The base class is PageTransition and it has four derivatives: These are called SlideTransition, SkewTransition, CurlTransition, and FlipTransition. In porting these to Windows 8, I discovered that the developer's pre-release is missing a couple essential features:
- The Windows 8 UIElement defines a Clip property of type RectangleGeometry rather than just Geometry as it is in WPF, Silverlight, and Windows Phone. This is an issue for my CurlTransition class.
- The Windows 8 UIElement does not define a Projection property, which is required by my FlipTransition class.
Of course, I'm not complaining about these omissions because when working with a developer's pre-release, we developers make an implicit deal with Microsoft. We acknowledge that the product is not perfect, that there are some rough edges and, in fact, the whole thing can be changed between now and that magical day when Windows 8 becomes ready for prime time. But I will note that I frittered away about a day's worth of work trying to animate a custom dependency property. The property in question is named FractionalBaseIndex and defined in PageTransition and it is at the core of transitioning from one page of the book to the next. The Windows Phone version of the PageViewer control creates a Storyboard and DoubleAnimation in code to target this property. Each of the four classes that derive from PageTransition handle changes to FractionalBaseIndex differently to implement their particular styles of page transitions.
Here is what I discovered:
- You cannot define a dependency property in an abstract class. Or rather you can, but the Windows 8 type system will not recognize it. (It took me a long time to discover this!)
- The second and third arguments to DependencyProperty.Register are of type string rather than Type. (This is likely to be fixed in the future.) For the second argument indicating the property type you can use strings like "Double" or "Int32" for the .NET basic types, but for custom types, it's safest to use "Object" regardless of the type. For the third parameter use typeof(MyClass).FullName. That'll be easy to change when Windows 8 dependency properties eventually use arguments of type Type.
- For a custom dependency object to be recognized by the Windows 8 type system, the class defining that property must be instantiated in XAML. If the class is only instantiated in code, the dependency property will not be recognized.
- You cannot target a custom dependency property with an animation.
I assume all of these issues will be fixed in the months ahead. I'm mentioning them not to complain but so you don't waste as much time as me getting them to work. After much anguish I decided to "fix" the last item with a class that linearly animates a double dependency property from its current value to a specified value:
public class DoubleAnimator
{
TimeSpan startTime = TimeSpan.Zero;
double startValue;
// These properties much be set before calling Begin!
public DependencyObject Target { set; get; }
public DependencyProperty Property { set; get; }
public double ToValue { set; get; }
public Duration Duration { set; get; }
// Optional
public Windows.UI.Xaml.EventHandler Completed { set; get; }
public void Begin()
{
CompositionTarget.Rendering += OnCompositionTargetRendering;
}
void OnCompositionTargetRendering(object sender, object args)
{
TimeSpan renderingTime = (args as RenderingEventArgs).RenderingTime;
if (startTime == TimeSpan.Zero)
{
startTime = renderingTime;
startValue = (double)Target.GetValue(Property);
}
else
{
double value = 0;
if (renderingTime - startTime > Duration.TimeSpan)
{
Target.SetValue(Property, ToValue);
CompositionTarget.Rendering -= OnCompositionTargetRendering;
if (Completed != null)
Completed(this, EventArgs.Empty);
}
else
{
double fraction = (double)(renderingTime - startTime).Ticks /
Duration.TimeSpan.Ticks;
value = startValue * (1 - fraction) + ToValue * fraction;
Target.SetValue(Property, value);
}
}
}
}
How does this animation work in practive? Well, not so good, and part of the reason is that I'm moving Canvas objects containing a bunch of TextBlock elements around the screen. In the version of BookViewer in Phree Book Reader for Windows Phone, I improved performance considerably by setting the CacheMode property of the container for the animated elements to BitmapCache. In Windows 8, another issue came up:
- The Windows 8 UIElement defines a CacheMode property, which you can set to an instance of either HardwareBitmapCache or SoftwareBitmapCache, but the documentation provides no information about which one to use in particular cases. Regardless, it is my experience that these sometime result in the element disappearing entirely.
You can always do your own bitmap caching using WriteableBitmap except, as we already know,
- The Windows 8 version of WriteableBitmap has no Render method.
Although Windows 8 may have some rough edges, The New Epublic is in much worse shape. Some kind of bug still exists in the page transition logic, and much else needs to be done. The next step will be to parse CSS files so at least I get page-break-before attributes for page breaking, and id attributes for navigating to chapter starts.