Don of the Day

Don of the Day


Adventures in software development with Xamarin and the Web

Software developer, building things with code in sunny Scottsdale, AZ.

Share


Twitter


Windows Phone Custom Live Tiles

Don FitzsimmonsDon Fitzsimmons

Live Tiles are one of the best things about Windows Phone. But, what I find frustrating about developing a Windows Phone app is the lack of Live Tile customization. The SDK does provide some defaults, but you’re limited to an icon and some text regions defined for each tile size. I guess the defaults are okay for most uses, but I really want my tiles to look the way I want them to look and that falls outside the predefined templates.

The good news is, you can create your own custom tiles. The bad news is, it’s a bit tricky, especially when you want to update those tiles. To create my custom tiles, this is what I did (after much frustration because of all of the gotchas):

That’s a lot of complexity and a lot of code, but for me it’s worth it to have my live tiles totally customized. So let’s get into it. Below is what my project structure looks like.

solution

I have my phone app project at the top, a scheduled task project, my portable class library and my tiles project. Again, the reason for giving the tiles their own project is because both the phone app and the scheduled task will need to reference my tiles and my tile logic.

Within the tile project there are 4 Windows Phone user controls that I created to represent my tiles. This is really nice because you can make use of XAML and the design surface to get your tiles looking just right. Each tile will need a front and back control. In my case, the back of each tile is static text with a custom background color, but the front has a few dynamic elements that need to updated frequently (or every 30 minutes as defined by the SDK).

This is what my medium tile user controls look like:

front tile in VS designerfront tile in VS designer

back tile in VS designerback tile in VS designer

That’s what my tiles look like in the VS designer (and yes, I’m building a countdown app). For each control, I set public properties so that I can set my values later.

public partial class MediumTile : UserControl { public MediumTile() { InitializeComponent(); } public string Name { set { this.uxName.Text = value; } get { return this.uxName.Text; } } public string Count { set { this.uxCountValue.Text = value; } get { return this.uxCountValue.Text; } } public Brush BackgroundColor { set { this.uxBackGroundCanvas.Background = value; } } public string TimeLabel { set { this.uxTimeLabel.Text = value; } } }

To generate my tiles, I created a tile manager class called…TileManager. This is where I instantiate my user controls, set values and then render and save the images (this is also where things get messy). Here’s the important bits and note that much of this code was adapted from the Nokia Development Wiki:

This is the method I call to create my tiles:

public static void SetDetailTile(CountdownItem item) { Uri navigationUri = new Uri("/Pages/Detail.xaml?id=" + item.Id.ToString() + "&from=tile", UriKind.Relative); var flipTileData = new FlipTileData { Title = "", BackContent = "", WideBackContent = "", BackTitle = "", Count = 0 }; //create medium and wide tile images RenderMediumTileImage(item); RenderWideTileImage(item); flipTileData.BackgroundImage = new Uri("isostore:/Shared/ShellContent/" + item.Id.ToString() + "-mediumtile.jpg"); flipTileData.BackBackgroundImage = new Uri("isostore:/Shared/ShellContent/" + item.Id.ToString() + "-mediumbacktile.jpg"); flipTileData.WideBackgroundImage = new Uri("isostore:/Shared/ShellContent/" + item.Id.ToString() + "-widetile.jpg"); flipTileData.WideBackBackgroundImage = new Uri("isostore:/Shared/ShellContent/" + item.Id.ToString() + "-widebacktile.jpg"); // Get the tile if it exists var tile = ShellTile.ActiveTiles.FirstOrDefault(t => t.NavigationUri navigationUri); // Create the tile if it doesn't exist if (tile null) { // create a new tile ShellTile.Create(navigationUri, flipTileData, true); } // Update existing tile else { tile.Update(flipTileData); } }

First I set the default content properties to empty strings, then I render the images (see below) and then I pull the images from isolated storage. *Important Note: * You must save and retrieve your custom tiles from the “/Shared/ShellContent” directory or you will not see images. For whatever reason, the tiles must come from that directory. This caused me hours of frustration and many a Google search to figure out. Once I have my images, I save or update my tiles.

But, before we retrieve images or save tiles, we must create images. I’m going to break this down a bit to explain each part.

private static void RenderMediumTileImage(CountdownItem item) { WriteableBitmap frontTile = new WriteableBitmap(336, 336); MediumTile tile = new MediumTile(); TimeSpan timeSpan = item.EndDate.Subtract(DateTime.Now); //other things that are app specific

Here I create a WritableBitmap that ultimately represents the image and get an instance of the tile control (MediumTile). Next I set some tile properties and do something very important.

tile.Name = item.Name.Length >= 18 ? item.Name.Substring(0, 18) + "..." : item.Name; tile.BackgroundColor = new SolidColorBrush(ColorHelper.ConvertColor(item.AccentColor)); tile.UpdateLayout(); tile.Measure(new Size(336, 336)); tile.UpdateLayout(); tile.Arrange(new Rect(0, 0, 336, 336)); frontTile.Render(tile.LayoutRoot, null); frontTile.Invalidate(); //Draw bitmap

This is critical. After setting your tile properties, you must update the layout, measure it, update again, arrange it and chant “Mekah lekah high, mekah hinney ho”. Okay, not the last part, but you really do have to all of this updating and arranging to make sure the control renders to the image properly. If you don’t, it will look funky. Next, more magical things that will cost you time and frustration if you are unaware.

backImage.Render(backTile.LayoutRoot, null); backImage.Invalidate(); SaveTileImage(item.Id.ToString() + "-mediumbacktile.jpg", backImage); backImage = null; frontTile = null; tile = null; backTile = null; GC.Collect();

Next, I save the Image (I’ll show the code for that below). The invalidate method of the WriteableBitmap actually draws it (oddly named, yes). The crucial part is what comes after the image is saved. Because I’m also going to call this method from my ScheduledTask, which has serious time and memory limitations, we have to GC.Collect() manually and I honestly don’t know why, but I do know that processing multiple tiles without manual garbage collection causes an OutOfMemoryException from the ScheduledTask. I can only assume the garbadge collector doesn’t free up the image resources fast enough, quickly maxing out the SheduledTasks meager allotment. Once I made the controls and the images null and collected the trash, all was well. Took me many hours to figure that one out too.

Oh, and here’s the code to save the image (again, it must be saved to “/Shared/ShellContent”):

private static void SaveTileImage(string fileName, WriteableBitmap b) { //Save bitmap as jpeg file in Isolated Storage using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication()) { using (IsolatedStorageFileStream imageStream = new IsolatedStorageFileStream("/Shared/ShellContent/" + fileName, System.IO.FileMode.Create, isf)) { b.SaveJpeg(imageStream, b.PixelWidth, b.PixelHeight, 0, 100); } } }

And the final gotcha: if you use a scheduled task to update your tiles, you must execute your tile update code on the UI thread because we are using user controls and they need their UI thread to work. See below.

protected override async void OnInvoke(ScheduledTask task) { try { var tiles = ShellTile.ActiveTiles.ToList(); CountdownViewModel vm = new CountdownViewModel(new CountdownService()); if (!vm.IsDataLoaded) await vm.LoadCountdownItems(); foreach (var tile in tiles) { string query = tile.NavigationUri.OriginalString; if (query != "/") { string id = tile.NavigationUri.ToString(); string[] idPart = id.Split(new string[] { "=" }, StringSplitOptions.None); var item = vm.GetCountdownItem(new Guid(idPart[1])); if (item != null) { EventWaitHandle Wait = new AutoResetEvent(false); //Execute in the context of the UI thread Deployment.Current.Dispatcher.BeginInvoke(() => { TileManager.SetDetailTile(item); Wait.Set(); }); Wait.WaitOne(); } } } #if DEBUG_AGENT ScheduledActionService.LaunchForTest(task.Name, TimeSpan.FromSeconds(30)); System.Diagnostics.Debug.WriteLine("Periodic task is started again: " + task.Name); #endif } catch (Exception ex) { //log the exception } finally { NotifyComplete(); } }

That’s a lot of hoops to jump through to get custom tiles, but in the end, it’s worth it, at least to me. To recap, the main gotchas are:

Software developer, building things with code in sunny Scottsdale, AZ.

Comments