Thursday, May 1, 2014

Windows Phone Live Tile Data Binding Challenges

I struggle with my Live Tiles.  It seems like such an easy proposition, and indeed, all of the samples make it look trivial!  As long as you are just setting text and a static image, you'll have no problem.  As soon as you want to generate dynamic images you will have problems!

There are a few things to remember.  First of all, you will usually want to use a UserControl for layout and rendering of your content, then use WriteableBitmap to encode and save the bitmap.  Ideally you should use an extension package to encode as PNG in order to get a transparent background (especially with this being so popular in WP 8.1).  As long as you generated the tile content from within the app you are probably able to run it from a click handler or within a dispatcher pretty easily (you must have a dispatcher to create a UserControl).  If you are in a background agent, or on a background thread in the main app then you will get a cross-thread access exception.  From the agent you can't just get the current dispatcher like you can from within a control.  Not exactly the same anyway.  Instead of getting it from another control, just take it from the current deployment:

Deployment.Current.Dispatcher.BeginInvoke()

That part's easy!  What's not so easy is what happens if you include an image in your control.  You'll notice that the rendered tile is blank where the image should be.  This is incredibly frustrating to figure out!  If you test from within the app it will look fine.  You might get sites telling you to just add arbitrary delays to fix this.  It might even work, but it's not a great way to do it.  Instead, you need to subscribe to the image's ImageOpened event.  Once this fires (or the ImageFailed fires) you know that it's ready to save as a bitmap.  This causes some issues with synchronization, but in an agent, remember that the agent isn't complete until you call NotifyComplete(), so just make sure it's wherever the code is actually done.

The next thing to worry about is databinding.  If you set the data context, you expect your fields to be bound!  Unfortunately, that's not always the case.  For example, any "simple" scenarios such as TextBlock.Text, Image.Source, or Border.Foreground will look fine.  Issues come in with "multi-level" scenarios such as ItemControl items and TextBlock Run's.  What I discovered is that the binding all starts to take place, which is to say, an ItemsControl will create four templated items if there are four items in the collection, but the individual fields will never bind to the context.  If you set a FallbackValue, you will see that come through, but never the bound value.  In the same vein, a TextBlock with Run elements for text control, will show only the FallbackValue for each Run.  It's incredibly frustrating!  Unfortunately, I haven't found a solution.  I tried binding to many different events to see when the actual binding takes place, and if I run the UserControl within a running instance of the app it looks perfect, but I had to give up from an agent.

The solution is to convert a TextBlock with Run elements to a StackPanel of TextBlock elements.  Even worse, for an ItemsControl you need to actually create static controls for each item which should be generated dynamically!  This looks stupid in the XAML, but works fine.  Key to remember, is that you can use array indexing within binding.  For example, if your ItemsSource would have been "People" and in your GeneratedItem template you bound to "Name", then you can bind to "People[0].Name".  Really!  It works.  Of course it will cause silent errors if you go higher in index than actual values, but it won't cause issues with the final image.

It would be great if Microsoft gave us a way to pass a control type and data context to an UpdateLiveTile method, but until then, you will save much frustration by remembering these lessons I've learned!

No comments:

Post a Comment