Real-World Secondary Tiles: Top Three Tips for Working with Secondary Tiles in Windows 8 Apps

splash-screen-logo_03A group of colleagues and I developed a Windows 8 reference application called “Contoso Food & Dining”.  This application shows restaurant information and reviews around some location.  Among other things, I owned the implementation of secondary tiles. 

Secondary tiles enable users to promote specific content by pinning it to their Start Menu.  Selecting the secondary tile through a touch or a click launches the parent app and deep-links to that pinned content.  For example, a Weather app may allow me to pin a specific city’s weather page to the Start Menu as a secondary tiles.  Then, when I click on that tile, it launches the Weather app and takes me straight to that city’s weather page. 

Here are my top 3 tricks that helped me work with secondary tiles in a real-world application. 

1. Don’t Forget to Unpin!

A great resource for learning Windows 8 Development is the hands-on labs for the Contoso Cookbook.  They give you a step-by-step walkthrough of implementing all of the major Windows 8 features, including the logic to pin a secondary tile.  However, their code doesn’t include the ability to unpin a secondary tile.  According to the guidelines on secondary tiles, you are supposed to provide this. 

Reading the guidelines and checklist for secondary tiles, one of the requirements is:

When the content in focus is already pinned, the app bar button icon and label should be changed to show an "unpin" command instead. This will attempt to remove the existing secondary tile, subject to user confirmation.

Let’s walk through how to implement the functionality for both pinning and unpinning a restaurant as a secondary tile.  First, I need to add buttons for pinning and unpinning the restaurant to the AppBar.  I added the following code into the XAML of the restaurant’s detail page:

<Page.BottomAppBar>
    <AppBar>
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
                <Button x:Name="PinSecondaryTileButton" Style="{StaticResource PinAppBarButtonStyle}" Click="PinSecondaryTileButton_Click" Visibility="{Binding PinSecondaryTileButtonVisibility}"/>
                <Button x:Name="UnpinSecondaryTileButton" Style="{StaticResource UnpinAppBarButtonStyle}" Click="UnpinSecondaryTileButton_Click" Visibility="{Binding UnpinSecondaryTileButtonVisibility}"/>
        </StackPanel>
    </AppBar>
</Page.BottomAppBar>

This creates buttons in the AppBar that look like this:

Pin  Unpin

Notice that in the XAML above, each button has a name so I can access it programmatically from my code-behind, as well as style, click, and visibility attributes.  Let’s discuss each of these in more depth: 

Style

I am specifying the style of each button to be bound to a static resource called “PinAppBarButtonStyle” or “UnpinAppBarButtonStyle”.  These are predefined AppBar button styles that were provided in StandardStyles.xaml.  If you are not familiar with StandardStyles.xaml, it is included in the Common folder in any Visual Studio XAML template for Windows Store apps.  You can leverage the styles defined in it which already follow the Windows UI design guidelines and will look professional and familiar to your users.  Definitely poke through StandardStyles.xaml if you have not done so already – there is styling for a variety of common AppBar buttons (like Play, Pause, Edit, Delete, Save, Refresh, Help, etc.) which all derive from the same base AppBar button style, which you could extend for your own custom styles as well. 

Click

On the Click event, I am specifying an event handler.  Here is the event handler code for clicking “pin” and “unpin”.

/// <summary>
/// Pins a tile on the user's start screen representing the current Restaurant
/// </summary>
/// <param name="sender">The Pin button</param>
/// <param name="e">The unused RoutedEventArgs</param>
private async void PinSecondaryTileButton_Click(object sender, RoutedEventArgs e)
{
    // Keep AppBar open until done.
    this.BottomAppBar.IsSticky = true;
    this.BottomAppBar.IsOpen = true;

    // Get the current restaurant.  
    var restaurant = this.m_ViewModel.Restaurant;

    // The restaurant logo that we will use for the secondary tile image is coming from Yelp, and it is not supported to
    // pass an http image to the SecondaryTile constructor.  So we have to copy it locally first.  
    var logoUri = await GetLocalImageAsync(restaurant.ImagePath, restaurant.Key);

    // Create secondary tile object.  
    var tile = new SecondaryTile(
            restaurant.Key,                 // Tile ID
            restaurant.Name,                // Tile short name
            restaurant.Name,                // Tile display name
            this.m_ViewModel.Restaurant.ProtocolActivationUri.AbsoluteUri, // Activation argument
            TileOptions.ShowNameOnLogo,     // Tile options
            logoUri                         // Tile logo URI
        );

    tile.ForegroundText = ForegroundText.Light;

    // Show secondary tile popup and create asynchronously.  
    bool isPinned = await tile.RequestCreateForSelectionAsync(GetElementRect((FrameworkElement)sender), Windows.UI.Popups.Placement.Above);

    if (isPinned)
    {
        Debug.WriteLine("Secondary tile successfully pinned");

        // Change button in AppBar from "Pin" to "Unpin"
        this.m_ViewModel.PinSecondaryTileButtonVisibility = Visibility.Collapsed;
        this.m_ViewModel.UnpinSecondaryTileButtonVisibility = Visibility.Visible;
    }
    else
    {
        Debug.WriteLine("Secondary tile creation FAIL");
    }

    // Close AppBar
    this.BottomAppBar.IsOpen = false;
    this.BottomAppBar.IsSticky = false;
}

/// <summary>
/// Unpins the tile on the user's start screen representing the current Restaurant
/// </summary>
/// <param name="sender">The Unpin button</param>
/// <param name="e">The unused RoutedEventArgs</param>
private async void UnpinSecondaryTileButton_Click(object sender, RoutedEventArgs e)
{
    // Keep AppBar open until done.
    this.BottomAppBar.IsSticky = true;
    this.BottomAppBar.IsOpen = true;

    // Check to see if this restaurant exists as a secondary tile and then unpin it
    string restaurantKey = this.m_ViewModel.Restaurant.Key;
    Button button = sender as Button;
    if (button != null)
    {
        if (Windows.UI.StartScreen.SecondaryTile.Exists(restaurantKey))
        {
            SecondaryTile secondaryTile = new SecondaryTile(restaurantKey);
            bool isUnpinned = await secondaryTile.RequestDeleteForSelectionAsync(GetElementRect((FrameworkElement)sender), Windows.UI.Popups.Placement.Above);

            if (isUnpinned)
            {
                Debug.WriteLine("Secondary tile successfully unpinned.");

                // Change button in AppBar from "Unpin" to "Pin"
                this.m_ViewModel.PinSecondaryTileButtonVisibility = Visibility.Visible;
                this.m_ViewModel.UnpinSecondaryTileButtonVisibility = Visibility.Collapsed;
            }
            else
            {
                Debug.WriteLine("Secondary tile not unpinned.");
            }
        }
        else
        {
            Debug.WriteLine(restaurantKey + " is not currently pinned.");

            // If we ever get to this point, something went wrong or the user manually removed the tile from
            // their Start screen.  The pin/unpin functionality is mixed up, so we will correct it now.  The
            // secondary tile doesn't exist so "Pin" should show.  
            this.m_ViewModel.PinSecondaryTileButtonVisibility = Visibility.Visible;
            this.m_ViewModel.UnpinSecondaryTileButtonVisibility = Visibility.Collapsed;
        }
    }

    // Close AppBar
    this.BottomAppBar.IsOpen = false;
    this.BottomAppBar.IsSticky = false;
}

 

Visibility

Finally, in my XAML, the buttons have visibility attributes which are bound to visibilities.  For example, the restaurant’s Pin button is bound to a PinSecondaryTileButtonVisibility.  We want this to be visible when the restaurant is not pinned and invisible/collapsed when the button is already pinned. 

The logic to change the visibilities when the button is pinned and unpinned is part of the event handler code that you just saw.  In addition, we are defining these visibilities in the restaurant details ViewModel:

private Visibility _pinSecondaryTileButtonVisibility = Visibility.Collapsed;
public Visibility PinSecondaryTileButtonVisibility
{
    get { return _pinSecondaryTileButtonVisibility; }
    set { this.SetProperty<Visibility>(ref this._pinSecondaryTileButtonVisibility, value, "PinSecondaryTileButtonVisibility"); }
}

private Visibility _unpinSecondaryTileButtonVisibility = Visibility.Collapsed;
public Visibility UnpinSecondaryTileButtonVisibility
{
    get { return _unpinSecondaryTileButtonVisibility; }
    set { this.SetProperty<Visibility>(ref this._unpinSecondaryTileButtonVisibility, value, "UnpinSecondaryTileButtonVisibility"); }
}

When we first load the restaurant details, we will initialize these visibilities:

// Set visibility for pin/unpin secondary tile
this.PinSecondaryTileButtonVisibility = SecondaryTile.Exists(this.Restaurant.Key) ? Visibility.Collapsed : Visibility.Visible;
this.UnpinSecondaryTileButtonVisibility = SecondaryTile.Exists(this.Restaurant.Key) ? Visibility.Visible : Visibility.Collapsed;

 

Handle activation argument on launch

The final piece that we need to get secondary tiles working is functionality that recognizes when the app is being called with an activation argument and responds with the deep-linking behavior of secondary tiles.  In the OnLaunched method in App.xaml.cs, we will check for arguments and if they exist, we will get the corresponding restaurant from the activation argument and then navigate to its page. 

// If the app was activated from a secondary tile, open to the correct restaurant
if (!String.IsNullOrEmpty(args.Arguments))
{
    var restaurant = await GetRestaurantFromSecondaryTile(args.Arguments);
    rootFrame.Navigate(typeof(RestaurantDetailsPage), restaurant);
}

 

2. Pinning a secondary tile where the logo is from the internet (http not ms-appx)

With this functionality that we’ve implemented, I can pin a restaurant tile to the Start Menu.  We are getting the restaurant images which are displayed on the restaurant tile from Yelp.  However, in the secondary tile constructor, you need to use a logo image which is located at a ms-appx or ms-appdata:///local/ URI.  This means that if I want to use an image from the internet for the secondary tile logo, I have to download it locally before I can use it.  

In my “Pin” click handler, I am calling a method called GetLocalImageAsync.  This method copies an image from the internet (http protocol) locally to the AppData LocalFolder.  Then it can be accessed using the ms-appdata:///local/ protocol and passed into the SecondaryTile constructor

/// <summary>
/// Copies an image from the internet (http protocol) locally to the AppData LocalFolder.  This is used by some methods
/// (like the SecondaryTile constructor) that do not support referencing images over http but can reference them using
/// the ms-appdata protocol.  
/// </summary>
/// <param name="internetUri">The path (URI) to the image on the internet</param>
/// <param name="uniqueName">A unique name for the local file</param>
/// <returns>Path to the image that has been copied locally</returns>
private async Task<Uri> GetLocalImageAsync(string internetUri, string uniqueName)
{
    if (string.IsNullOrEmpty(internetUri))
    {
        return null;
    }

    using (var response = await HttpWebRequest.CreateHttp(internetUri).GetResponseAsync())
    {
        using (var stream = response.GetResponseStream())
        {
            var desiredName = string.Format("{0}.jpg", uniqueName);
            var file = await ApplicationData.Current.LocalFolder.CreateFileAsync(desiredName, CreationCollisionOption.ReplaceExisting);

            using (var filestream = await file.OpenStreamForWriteAsync())
            {
                await stream.CopyToAsync(filestream);
                return new Uri(string.Format("ms-appdata:///local/{0}.jpg", uniqueName), UriKind.Absolute);
            }
        }
    }
}

 

 

3. How to debug secondary tiles

There is a trick to debugging secondary tiles.  In Visual Studio, you can easily set breakpoints, hit F5 to run the app, and step through pinning the secondary tile to the Start Menu.  But when you launch the secondary tile from the Start Menu, you won’t have Visual Studio attached to the process by default.  If you want to step through this logic, you can set Visual Studio to wait to attach to your Windows Store app. 

Right-click on the C# executable project and select Properties.  In the left sidebar, select the Debug tab.  In the “Start Action” section, check the box for “Do not launch, but debug my code when it starts”.  Then you can press F5 to run and Visual Studio will wait; when you click the secondary tile from the Start Menu to invoke the app, Visual Studio will attach to the process and you can debug. 

DoNotLaunch

 

Secondary Tile Reference

Finally, here are some further resources on secondary tiles. 

 

Are you interested in further tips on Windows 8 development?  Sign up for the 30 to launch program which will help you build an application in 30 days.  You will receive a tip per day for 30 days, along with potential free design consultations and technical support from a Windows 8 expert.  (See https://aka.ms/JenGenApp and click on “Coders”).  Register at https://aka.ms/Jen30TLWin8.