Developing for Modern Windows

Tips, tricks, and guides for developing on modern Windows platforms

XAML to Live Tile in UWP Apps

In this article I’ll show you how to create an image from a XAML user control, and then set that image as the app’s live tile. For this example, I’ll update the wide and medium tiles, but you can adapt it to suit more tile sizes (and scales).

This method works for Universal Windows Platform apps, and is tested on PC and phone.

How we’ll do it

  1. Create a XAML control that forms your tile’s look and content.
  2. Take a ‘snapshot’ of that XAML control and save it as a .PNG image.
  3. Update the live tile using the saved image(s) as the tile content.

Download

Rather than detail every step, I’ve prepared a full working example project, and the entire helper class, which that you can paste into any UWP project, is later in the article.

Full Demo Project

Step 1: Create a Tile XAML Control

You need to create XAML controls for each tile size you want to build. Design the XAML controls to look exactly as you want, using anything you want. In the example project I created a TileContent class that contains an image, a title string, a number, and a background colour, which I bound to the tiles.

The XAML control you render to .PNG must be in the visual tree (i.e. on the page somewhere), but it doesn’t have to be visible to the user (it can be behind other content). Hide your tiles behind the scenes so the user can’t see them. You can see this behaviour in action by adding an opaque <Grid> in front of the tiles in the demo project.

Canvas Caveats

Through trial-and-error I found that I needed to build my controls in a certain way for them to render correctly as .PNG images. I used a <Canvas> as the root element, and needed an image to define the actual layout size. I therefore used a dummy image and set its size to be exactly my output desired tile size (e.g. 336x336px for the square tile). I put my actual content in front of this image.

Here’s what the square tile XAML looks like from the sample project:

<Canvas>
    <Image Source="/Images/background.png" Width="336" Height="336"/>
    <Grid Height="336" Width="336" Background="{Binding BackgroundColour}"/>
    <Image Source="{Binding Img}" HorizontalAlignment="Center" VerticalAlignment="Center" Width="200" Height="200"/>
        <TextBlock Text="{Binding Title}" HorizontalAlignment="Center" VerticalAlignment="Top" FontSize="32" FontWeight="Bold"/>
        <TextBlock Text="{Binding Number}" VerticalAlignment="Bottom" HorizontalAlignment="Center" FontSize="48" FontWeight="Bold"/>
    </Grid>
</Canvas>

In the sample project, that tile looks like this (when some data is bound to the control):

Step 2: Render the XAML controls to images

Here’s the method for converting a FrameworkElement (a XAML control) to a .PNG image. There’s not a huge amount of complexity – we just need to create/open a file, then stream the pixel information via a bitmap encoder, then close the file when we’re done.

static async Task GenerateImageToFile(string fileName, FrameworkElement uiContent)
        {       
            var file = await ApplicationData.Current.LocalFolder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting);
           if(file != null)
           {
               CachedFileManager.DeferUpdates(file);
               using(var stream = await file.OpenAsync(FileAccessMode.ReadWrite))
               {
                   await CaptureToStreamAsync(uiContent, stream, BitmapEncoder.PngEncoderId);
               }
                await CachedFileManager.CompleteUpdatesAsync(file);
            }
        }

static async Task <RenderTargetBitmap> CaptureToStreamAsync(FrameworkElement uielement, IRandomAccessStream stream, Guid encoderId)
            {
                var renderTargetBitmap = new RenderTargetBitmap();
                await renderTargetBitmap.RenderAsync(uielement);
                IBuffer pixels = await renderTargetBitmap.GetPixelsAsync();
                double logicalDpi = DisplayInformation.GetForCurrentView().LogicalDpi;
               var encoder = await BitmapEncoder.CreateAsync(encoderId, stream);
               encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Straight, (UInt32)(renderTargetBitmap.PixelWidth), (UInt32)(renderTargetBitmap.PixelHeight), logicalDpi, logicalDpi, pixels.ToArray());
               await encoder.FlushAsync();
               return renderTargetBitmap;
         }

Using this code, the images are stored in the LocalState folder of your app’s install location (<user>/AppData/Local/Packages/<app>). For filename, do not include any folder information, just use, e.g. ‘mediumTile.png’.

It’s important to not do this until you know the XAML control is drawn (otherwise the resulting image might be incomplete or even blank). Work out a good time to render your images, a time you can be sure the XAML has completely drawn (e.g. NOT during the page constructor).

Step 3: Set the live tile images

Now that you have saved the images you need a way to set them as the live tile content. I created the following method to set your saved images as the tile images using the image-only tile templates:

 static void SetLiveTileToSingleImage(string wideImageFileName, string mediumImageFileName)
            {
                // Construct the tile content as a string
                string content = $@"
                                <tile>
                                    <visual> 
                                        <binding template='TileSquareImage'>
                                           <image id='1' src='ms-appdata:///local/{mediumImageFileName}' />
                                        </binding> 
                                         <binding template='TileWideImage' branding='none'>
                                           <image id='1' src='ms-appdata:///local/{wideImageFileName}' />
                                        </binding>
 
                                    </visual>
                                </tile>";

                // Load the string into an XmlDocument
                XmlDocument doc = new XmlDocument();
                doc.LoadXml(content);

                // Then create the tile notification
                var notification = new TileNotification(doc);
                TileUpdateManager.CreateTileUpdaterForApplication().Update(notification);
            }

We build an XML string, which is what tells the live tile how to look. Note that we can inject the filenames into the XML using the curly braces, e.g. {mediumImageFileName}. Then we trigger a live tile update so the new content is pushed to the tile.

Streamline it

I like it when I can use a single line of code to achieve a single outcome, so let’s add one more method to the helper class:

public static async Task ConvertControlsToTiles(FrameworkElement wideTile, FrameworkElement mediumTile)
            {
                await GenerateImageToFile("wideTile.png", wideTile);
                await GenerateImageToFile("mediumTile.png", mediumTile);
                SetLiveTileToSingleImage("wideTile.png", "mediumTile.png");
            }

Now you can just call a single method with the two XAML controls as parameters to set the live tiles. This is the only public method in the helper, which keeps everything neat.

Once you add the helper class to your project and have your XAML tile templates set up, you can update the tiles with a single line of code:

await LivetileBuilder.ConvertControlsToTiles(wideControl, mediumControl);

The Helper Class

Here’s the entire helper class code (it’s the same code as in the demo project). You can add this to any UWP project, and it’s all you need (well, you also need to create your XAML live tile controls).

using System;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.Data.Xml.Dom;
using Windows.Graphics.Display;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.UI.Notifications;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media.Imaging;

namespace LiveTileUWP
{
    
        public class LiveTileBuilder
        {
            public static async Task ConvertControlsToTiles(FrameworkElement wideTile, FrameworkElement mediumTile)
            {
                await GenerateImageToFile("wideTile.png", wideTile);
                await GenerateImageToFile("mediumTile.png", mediumTile);
                SetLiveTileToSingleImage("wideTile.png", "mediumTile.png");
            }

            static async Task GenerateImageToFile(string fileName, FrameworkElement uiContent)
            {

                var file = await ApplicationData.Current.LocalFolder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting);
                if (file != null)
                {
                    CachedFileManager.DeferUpdates(file);
                    using (var stream = await file.OpenAsync(FileAccessMode.ReadWrite))
                    {
                        await CaptureToStreamAsync(uiContent, stream, BitmapEncoder.PngEncoderId);
                    }
                    await CachedFileManager.CompleteUpdatesAsync(file);
                }
            }

            static async Task<RenderTargetBitmap> CaptureToStreamAsync(FrameworkElement uielement, IRandomAccessStream stream, Guid encoderId)
            {
                var renderTargetBitmap = new RenderTargetBitmap();
                await renderTargetBitmap.RenderAsync(uielement);
                IBuffer pixels = await renderTargetBitmap.GetPixelsAsync();
                double logicalDpi = DisplayInformation.GetForCurrentView().LogicalDpi;
                var encoder = await BitmapEncoder.CreateAsync(encoderId, stream);
                encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Straight, (UInt32)(renderTargetBitmap.PixelWidth), (UInt32)(renderTargetBitmap.PixelHeight), logicalDpi, logicalDpi, pixels.ToArray());
                await encoder.FlushAsync();
                return renderTargetBitmap;
            }

            static void SetLiveTileToSingleImage(string wideImageFileName, string mediumImageFileName)
            {
                // Construct the tile content as a string
                string content = $@"
                                <tile>
                                    <visual> 
                                        <binding template='TileSquareImage'>
                                           <image id='1' src='ms-appdata:///local/{mediumImageFileName}' />
                                        </binding> 
                                         <binding template='TileWideImage' branding='none'>
                                           <image id='1' src='ms-appdata:///local/{wideImageFileName}' />
                                        </binding>
 
                                    </visual>
                                </tile>";

                // Load the string into an XmlDocument
                XmlDocument doc = new XmlDocument();
                doc.LoadXml(content);

                // Then create the tile notification
                var notification = new TileNotification(doc);
                TileUpdateManager.CreateTileUpdaterForApplication().Update(notification);
            }
        }
    }


What Next?

As I hinted earlier, you can expand this technique to cover different tile sizes (and also different tile scales). You can find the tile XML for different templates on the Windows Dev Center site. You might also try overloading the SetLiveTileToSingleImage() method to work with different tile layouts or sizes.

Let me know in the comments if you have any ideas to improve this functionality.

Tags: , , , ,

2 Responses to “XAML to Live Tile in UWP Apps”

  1. detrexer says:

    While the Idea is cute, the main problem using this method is that you can’t do it in a background service, since there isn’t a visual tree to render the xaml in as far as i know. Do you know any way around this?

Leave a Reply

Your email address will not be published.