MetroGridHelper for WinRT, a helper to get the alignment right

It’s already more than a year ago that Jeff Wilcox wrote the MetroGridHelper for Windows Phone. I’ve been using it a lot since then. But we now have the option to build WinRT applications, sadly without the MetroGridHelper.

Wait no more, I ported the code from Jeff and also modified it to fit the metro guidelines that exist for WinRT applications. I’ve had some help from Martin Tirion and Matthijs Hoekstra, both working as Evangelist for Microsoft in The Netherlands.

The result is something that helps me during the design of the Windows 8, WinRT, apps.

You can simply install the NuGet Package. Through the console like this:

PM> Install-Package WinRT.MetroGridHelper

And inside your App.xaml.cs you can add the following to the end of the OnLaunched method.

// Place the frame in the current Window and ensure that it is active
Window.Current.Content = rootFrame;
Window.Current.Activate();

//Only when the debugger is attached
if (System.Diagnostics.Debugger.IsAttached)
{
    //Display the metro grid helper
    MC.MetroGridHelper.MetroGridHelper.CreateGrid();
}

screenshot_07252012_204308

screenshot_07252012_204316

screenshot_07252012_204328

The current version only works for WinRT apps that are written in Xaml, though I’m investigating on how to create something similar within HTML5+JS. If you have suggestions for this version please let me know, and I will adjust the MetroGridHelper if they are helpful.

To be complete, you can use the below source code as an alternative to the NuGet Package.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using Windows.UI;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;

namespace MC.MetroGridHelper
{
    /// <summary>
    /// A utility class that overlays a designer-friendly grid on top of the
    /// application frame, for use similar to the performance counters in
    /// App.xaml.cs. The color and opacity are configurable. The grid contains
    /// a number of squares that are 24x24, offset with 12px gutters, and all
    /// 24px away from the edge of the device.
    /// </summary>
    public static class MetroGridHelper
    {
        private static bool _visible;
        private static double _opacity = 0.15;
        private static Color _color = Colors.Red;
        private static List<Shape> _shapes;
        private static Grid _grid;

        /// <summary>
        /// Gets or sets a value indicating whether the designer grid is
        /// visible on top of the application's frame.
        /// </summary>
        public static bool IsVisible
        {
            get { return _visible; }
            set
            {
                _visible = value;
                UpdateGrid();
            }
        }

        /// <summary>
        /// Gets or sets the color to use for the grid's squares.
        /// </summary>
        public static Color Color
        {
            get { return _color; }
            set
            {
                _color = value;
                UpdateGrid();
            }
        }

        /// <summary>
        /// Gets or sets a value indicating the opacity for the grid's squares.
        /// </summary>
        public static double Opacity
        {
            get { return _opacity; }
            set
            {
                _opacity = value;
                UpdateGrid();
            }
        }

        /// <summary>
        /// Updates the grid (if it already has been created) or initializes it
        /// otherwise.
        /// </summary>
        private static void UpdateGrid()
        {
            if (_shapes != null)
            {
                var brush = new SolidColorBrush(_color);
                foreach (Shape square in _shapes)
                {
                    square.Fill = brush;
                }
                if (_grid != null)
                {
                    _grid.Visibility = _visible ? Visibility.Visible : Visibility.Collapsed;
                    _grid.Opacity = _opacity;
                }
            }
            else
            {
                BuildGrid();
            }
        }

        /// <summary>
        /// Builds the grid.
        /// </summary>
        private static async void BuildGrid()
        {
            _shapes = new List<Shape>();

            var frame = Window.Current.Content as Frame;
            if (frame == null || VisualTreeHelper.GetChildrenCount(frame) == 0)
            {
                await Window.Current.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, BuildGrid);
                return;
            }

            DependencyObject child = VisualTreeHelper.GetChild(frame, 0);
            var childAsBorder = child as Border;
            var childAsGrid = child as Grid;
            if (childAsBorder != null)
            {
                // Not a pretty way to control the root visual, but I did not
                // want to implement using a popup.
                UIElement content = childAsBorder.Child;
                if (content == null)
                {
                    await Window.Current.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, BuildGrid);
                    return;
                }
                childAsBorder.Child = null;
                await Window.Current.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
                                                                                            {
                                                                                                var newGrid = new Grid();
                                                                                                childAsBorder.Child =
                                                                                                    newGrid;
                                                                                                newGrid.Children.Add(
                                                                                                    content);
                                                                                                PrepareGrid(frame,
                                                                                                            newGrid);
                                                                                            });
            }
            else if (childAsGrid != null)
            {
                PrepareGrid(frame, childAsGrid);
            }
            else
            {
                Debug.WriteLine("Dear developer:");
                Debug.WriteLine("Unfortunately the design overlay feature requires that the root frame visual");
                Debug.WriteLine("be a Border or a Grid. So the overlay grid just isn't going to happen.");
            }
        }

        public static void CreateGrid()
        {
            IsVisible = true;
        }

        /// <summary>
        /// Does the actual work of preparing the grid once the parent frame is
        /// in the visual tree and we have a Grid instance to work with for
        /// placing the chilren.
        /// </summary>
        /// <param name="frame">The phone application frame.</param>
        /// <param name="parent">The parent grid to insert the sub-grid into.</param>
        private static void PrepareGrid(Frame frame, Grid parent)
        {
            var brush = new SolidColorBrush(_color);

            _grid = new Grid {IsHitTestVisible = false};

            // To support both orientations, unfortunately more visuals need to
            // be used. An alternate implementation would be to react to the
            // orientation change event and re-draw/remove squares.
            double width = frame.ActualWidth;
            double height = frame.ActualHeight;
            double max = Math.Max(width, height);

            const double strokeWidth = 2.0;

            var horizontalLine = new Line
                                     {
                                         IsHitTestVisible = false,
                                         Stroke = brush,
                                         X1 = 0,
                                         X2 = max,
                                         Y1 = 100 + (strokeWidth/2),
                                         Y2 = 100 + (strokeWidth/2),
                                         StrokeThickness = strokeWidth,
                                     };
            _grid.Children.Add(horizontalLine);
            _shapes.Add(horizontalLine);
            var horizontalLine2 = new Line
                                      {
                                          IsHitTestVisible = false,
                                          Stroke = brush,
                                          X1 = 0,
                                          X2 = max,
                                          Y1 = 140 + (strokeWidth/2),
                                          Y2 = 140 + (strokeWidth/2),
                                          StrokeThickness = strokeWidth,
                                      };
            _grid.Children.Add(horizontalLine2);
            _shapes.Add(horizontalLine2);

            var verticalLine = new Line
                                   {
                                       IsHitTestVisible = false,
                                       Stroke = brush,
                                       X1 = 120 - (strokeWidth/2),
                                       X2 = 120 - (strokeWidth/2),
                                       Y1 = 0,
                                       Y2 = max,
                                       StrokeThickness = strokeWidth,
                                   };
            _grid.Children.Add(verticalLine);
            _shapes.Add(verticalLine);

            var horizontalBottomLine = new Line
            {
                IsHitTestVisible = false,
                Stroke = brush,
                X1 = 0,
                X2 = max,
                Y1 = height - 130 + (strokeWidth / 2),
                Y2 = height - 130 + (strokeWidth / 2),
                StrokeThickness = strokeWidth,
            };
            _grid.Children.Add(horizontalBottomLine);
            _shapes.Add(horizontalBottomLine);
            var horizontalBottomLine2 = new Line
            {
                IsHitTestVisible = false,
                Stroke = brush,
                X1 = 0,
                X2 = max,
                Y1 = height - 50 + (strokeWidth / 2),
                Y2 = height - 50 + (strokeWidth / 2),
                StrokeThickness = strokeWidth,
            };
            _grid.Children.Add(horizontalBottomLine2);
            _shapes.Add(horizontalBottomLine2);

            const int tileHeight = 20;

            for (int x = 120; x < /*width*/ max; x += (tileHeight*2))
            {
                for (int y = 140; y < /*height*/ max; y += (tileHeight*2))
                {
                    var rect = new Rectangle
                                   {
                                       Width = tileHeight,
                                       Height = tileHeight,
                                       VerticalAlignment = VerticalAlignment.Top,
                                       HorizontalAlignment = HorizontalAlignment.Left,
                                       Margin = new Thickness(x, y, 0, 0),
                                       IsHitTestVisible = false,
                                       Fill = brush,
                                   };
                    _grid.Children.Add(rect);
                    _shapes.Add(rect);
                }
            }

            _grid.Visibility = _visible ? Visibility.Visible : Visibility.Collapsed;
            _grid.Opacity = _opacity;

            // For performance reasons a single surface should ideally be used
            // for the grid.
            _grid.CacheMode = new BitmapCache();

            // Places the grid into the visual tree. It is never removed once
            // being added.
            parent.Children.Add(_grid);
        }
    }
}

What icon sizes are important for your Win RT app?

The are quite a couple of icons that you can set, though not all of them are absolutely required.

For the tile we have the following logo sizes. I also created the example of an app I’m working on.

The main tile requires a logo of 150x150 pixels.

Logo

Then we have a wide logo that’s only required when you want your app to be visible with a wide logo on the Start screen. When you use it, the size has 310x150 pixels. That’s two tile logo’s + a 10 pixel margin.

WideLogo

Then we have a small logo that’s used in all kinds of views inside Windows like search, share and more. It has to be 30x30 pixels.

SmallLogo

Next we have a badge logo. This is logo that can be shown on the lockscreen when there are notifications from the app, and as of this it’s only required if you want to enable showing your badge on the lockscreen. It has to be a monochromatic logo of 24x24 pixels.

BadgeLogo

We shouldn’t forget the splashscreen, it isn’t really a logo, but often does contain the logo. Besides there are constraints about size here as well, being 620x300 pixels.

SplashScreen

And in the end the Store logo is what’s left. Interestingly this is something you configure as part of your App package, though it’s on the Packaging tab while the rest is on the Application UI tab. From Windows Phone we are used to supply this icon during submission. This icon has to be 50x50 pixels.

StoreLogo

I’m sure you can come up with more creative icons, but I hope you now have an understanding of all the icons that you have to create.

Google Analytics Real Time tracking for Windows Phone, who’s using my app right now?

Interested to see what happens right after you send everyone a push notification? I am!

At first I thought it wasn’t possible to do Page tracking from within the MSAF + Google Analytics. But after a little bit of investigation I saw that it was always overwritten by MSAF, a small modification made it possible.

The NuGet Package I created (now updated) has new methods to track the page on the AnalyticsTracker helper-class.

public interface IAnalyticsTracker
{
    void Track(string category, string name);
    void Track(string category, string name, string label);
    void TrackPage(Uri pageUri);
    void TrackPage(string page);
}

I now have a simple extra call inside the OnNavigatedTo methods on the Page.

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);
    var analyticsTracker = new AnalyticsTracker();
    analyticsTracker.TrackPage(e.Uri);
}

And the result with testing both my emulator and on the phone itself was this image.

image

Try it out yourself.

NuGet packages are powerful, but the Transformations are not. Time for a solution!

First of all I don’t want to complain about NuGet. I love it every day and I have created two packages myself that I want be easy to use for everyone. It should be a matter of just Install-Package PackageName and not much more.

I currently have the following packages.

It was last week when I wanted to update the Google Analytics package that I found a problem. When the package was used in a previous version, and I run the update it did a strange thing in the transformation I made before. The transformation I created was transforming the App.xaml file by adding an element to the xaml.

<GoogleAnalyticsService WebPropertyId="UA-12345-6" xmlns="clr-namespace:$rootnamespace$.Analytics" />

The transformation in NuGet aren’t really intelligent. It just adds the above element. And when you have an update for the package it will ad the element again. That’s something I would like to be able to prevent. So are the solutions at the table? Yes there are, you can use PowerShell.

So I took a little bit of time to learn more about PowerShell. I knew it was something like batch, but with support for .NET Libraries. The support for .NET Libraries implies easy use of XML editing, which will be handy Xaml is just Xml.

The first step I had to take is be able to read the content of the App.xaml file from the PowerShell script that can be used as part of the NuGet Package. I’m able to iterate through the project items to find the App.xaml project item which in turn helps me find the local path.

param($installPath, $toolsPath, $package, $project)

# find the App.xaml file 
$config = $project.ProjectItems | where {$_.Name -eq "App.xaml"}

# find its path on the file system 
$localPath = $config.Properties | where {$_.Name -eq "LocalPath"}

That wasn’t that hard, was it? Let’s read the App.xaml file into an Xml object. And look for an element with GoogleAnalyticsService as the name.

# load Web.config as XML 
$xml = New-Object xml 
$xml.Load($localPath.Value)

$gasnode = $xml.SelectNodes("//*") | where { $_.Name -eq "GoogleAnalyticsService"} 
if($gasnode -eq $null) 
{ 
} 

If we didn’t find that kind of element we want to create the element with the example WebPropertyId, just like I previously had in the App.xaml NuGet transformation. First I select the proposed parent element, being the “Application.ApplicationLifetimeObjects” element which I can use to add the to be created XmlNode to. Next I create the XmlNode in the appropriate namespace and add the element the parent element. What’s left is to save the App.xaml file.

$lto = $xml.SelectNodes("//*") | where { $_.Name -eq "Application.ApplicationLifetimeObjects"} 
$gas = $xml.CreateNode("element","analytics:GoogleAnalyticsService","clr-namespace:"+$project.Properties.Item("RootNamespace").Value+".Analytics") 
$gas.SetAttribute("WebPropertyId", "UA-12345-6") 
$lto.AppendChild($gas) 
# save the App.xaml file 
$xml.Save($localPath.Value)

And to be complete, this was the complete Install.ps1 file. Hope this helps all the people who have been struggling to solve the limited Transformations issue.

param($installPath, $toolsPath, $package, $project)

# find the App.xaml file 
$config = $project.ProjectItems | where {$_.Name -eq "App.xaml"}

# find its path on the file system 
$localPath = $config.Properties | where {$_.Name -eq "LocalPath"}

# load Web.config as XML 
$xml = New-Object xml 
$xml.Load($localPath.Value)

$gasnode = $xml.SelectNodes("//*") | where { $_.Name -eq "GoogleAnalyticsService"} 
if($gasnode -eq $null) 
{ 
  $lto = $xml.SelectNodes("//*") | where { $_.Name -eq "Application.ApplicationLifetimeObjects"} 
  $gas = $xml.CreateNode("element","analytics:GoogleAnalyticsService","clr-namespace:"+$project.Properties.Item("RootNamespace").Value+".Analytics") 
  $gas.SetAttribute("WebPropertyId", "UA-12345-6") 
  $lto.AppendChild($gas) 
  # save the App.xaml file 
  $xml.Save($localPath.Value) 
}