Thursday, 28 November 2013

Creating Azure Notifications Hub Push Notifications using Node Script

Windows Azure Mobile Services has great built-in support for making Push Notifications; however there’s a better more scalable option using the Notifications Hub which is part of the Service Bus group of services.
We can call the hub from external backend services using the Windows Azure Service Bus SDK (NuGet PM> Install-Package WindowsAzure.ServiceBus) and this is fairly well documented; however if you want to do a direct replacement for the build-in Push Notifications using Notifications Hub, it’s not so well documented. I did some digging in the source to work out how to use it: https://github.com/WindowsAzure/azure-sdk-for-node
In our Mobile Services scripts we can make use of NPM packages which we install ourselves and pre-installed packages like the Windows Azure SDK for Node which we will use for calling the Hub.

Accessing the Notifications Hub

We need two things to allow us to connect to the hub and those are the DefaultFullSharedAccessSignature which you can get from the Notifications Hub Dashboard and the Hub name. We can declare these as variables like this:

var CONNECTION_STRING = "Endpoint=sb://myapp.servicebus.windows.net/;SharedAccessKeyName=DefaultFullSharedAccessSignature;SharedAccessKey=XXXXXXXXXXXXXXXXXXXXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=";
var HUB_NAME = "MyHubName";

Now to interact with the Notifications hub, we get a reference to the Azure package like this:

var azure = require("azure");

Then create a NotificationHubService like this:

var notificationHubService = azure.createNotificationHubService(HUB_NAME, CONNECTION_STRING);

All the native PNS send methods have prototypes similar to this:

MpnsService.prototype.send = function (tags, payload, targetName, notificationClass, optionsOrCallback, callback)

The Notifications class is the batching interval (how quick it’s sent).

Windows Phone MPNS Notofications

Following are examples of sending toast and tiles to Windows Phone apps using the MPNS provider:   

function sendToastHubMpns(text1, text2, tagExpression)
{
    var azure = require("azure");
    var notificationHubService = azure.createNotificationHubService(HUB_NAME, CONNECTION_STRING);
   
    var toast = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
    "<wp:Notification xmlns:wp=\"WPNotification\">" +
        "<wp:Toast>" +
            "<wp:Text1>" + text1 + "</wp:Text1>" +
            "<wp:Text2>" + text2 + "</wp:Text2>" +
        "</wp:Toast> " +
    "</wp:Notification>";
   
    notificationHubService.mpns.send(tagExpression, toast, "toast", 2, function(error) {
    if (error) {
        console.error(error);
    }});
}

function sendTileHubMpns(backTitle, backContent, tagExpression)
{
  var azure = require("azure");
  var notificationHubService = azure.createNotificationHubService(HUB_NAME, CONNECTION_STRING);
   
  var tile = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
      "<wp:Notification xmlns:wp=\"WPNotification\" Version=\"2.0\">" +
         "<wp:Tile Template=\"FlipTile\">" +
              "<wp:BackTitle>" + backTitle + "</wp:BackTitle>" +
              "<wp:BackContent>" + backContent + "</wp:BackContent>" +
              "<wp:WideBackContent>" + backContent + "</wp:WideBackContent>" +
         "</wp:Tile> " +
      "</wp:Notification>";
 
    notificationHubService.mpns.send(tagExpression, tile, "token", 1, function(error) {
    if (error) {
        console.error(error);
    }});
}

Windows 8 WNS Notifications

Following are examples of sending toast, tile and badge notifications to Windows 8 apps using the WNS provider:   

function sendToastHubWns(text1, text2, text3, tagExpression)
{
  var azure = require("azure");
  var notificationHubService = azure.createNotificationHubService(HUB_NAME, CONNECTION_STRING);
 
  var toast = "<toast>" +
    "<visual>" +
      "<binding template=\"ToastText04\">" +
        "<text id=\"1\">" + text1 + "</text>" +
        "<text id=\"2\">" + text2 + "</text>" +
        "<text id=\"3\">" + text3 + "</text>" +
        "</binding>" +
      "</visual>" +
    "</toast>";

    notificationHubService.wns.send(tagExpression, toast, "wns/toast", 2, function(error) {
    if (error) {
        console.error(error);
    }});
}

function sendTileHubWns(text1, text2, text3, tagExpression)
{
    var azure = require("azure");
    var notificationHubService = azure.createNotificationHubService(HUB_NAME, CONNECTION_STRING);
   
    var tile = "<tile>" +
    "<visual>" +
      "<binding template=\"TileSquareText01\">" +
        "<text id=\"1\">" + text1 + "</text>" +
        "<text id=\"2\">" + text2 + "</text>" +
        "<text id=\"3\">" + text3 + "</text>" +
      "</binding>" +
    "</visual>" +
    "</tile>";
 
    notificationHubService.wns.send(tagExpression, tile, "wns/tile", 2, function(error) {
    if (error) {
        console.error(error);
    }});
}

function sendBadgeHubWns(value, tagExpression)
{
    var azure = require("azure");
    var notificationHubService = azure.createNotificationHubService(HUB_NAME, CONNECTION_STRING);
   
    var badge = "<badge value=\"" + value + "\" />";
 
    notificationHubService.wns.send(tagExpression, badge, "wns/badge", 2, function(error) {
    if (error) {
        console.error(error);
    }});
}

These functions can be called like this:

function sendAllHubNotifications(message)
{
  sendToastHubMpns("My Application", message, null);
  sendTileHubMpns("My Application ", message, null);

  sendToastHubWns("My Application ", "Here’s a notification", message, null);
  sendTileHubWns("My Application ", " Here’s a notification ", message, null;
  sendBadgeHubWns("alert",  null);
}

Notice there is a tagExpression variable which we have set to null. We can use this to target notifications at user’s interests which are registered in the app:

function sendAllHubNotifications(message)
{
  sendToastHubWns("My Application ", "Here’s a notification", message, "NEWS");
  sendTileHubWns("My Application ", " Here’s a notification ", message, "NEWS");
  sendBadgeHubWns("alert", "NEWS");
}

I managed to get Windows 8 to work with this but not Windows Phone 8

Registering with the Hub from a Windows 8 App

This example shows how to register a Windows 8 Store App with the DefaultListenSharedAccessSignature connection string this time (from the Notifocations Hub Dashboard). This registers two tags, but you can use null to receive everything:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.WindowsAzure.MobileServices;
using Newtonsoft.Json.Linq;
using Microsoft.WindowsAzure.Messaging;

namespace MyApp
{
    internal class MyAppPush
    {
        private const string HUB_NAME = "myapp";
        private const string CONNECTION_STRING = "Endpoint=sb://myapp.servicebus.windows.net/;SharedAccessKeyName=DefaultListenSharedAccessSignature;SharedAccessKey=/XXXXXXXXXXXXXXXXXXXXXXXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=";

        public async static void UploadChannel()
        {
            var channel = await Windows.Networking.PushNotifications.PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();

            var token = Windows.System.Profile.HardwareIdentification.GetPackageSpecificToken(null);
            string installationId = Windows.Security.Cryptography.CryptographicBuffer.EncodeToBase64String(token.Id);

            try
            {
                // Register with hub
                var hub = new NotificationHub(HUB_NAME, CONNECTION_STRING);
                var result = await hub.RegisterNativeAsync(channel.Uri, new string[] { "NEWS", "SUPPORT" });
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.ToString());
            }
        }
    }
}


Windows Phone 8 apps are registered in the same way, but they have a slightly different mechanism as they have an event when the URI changes which needs to be used for registration.

Thursday, 14 November 2013

Sharing a file from WP8 using Bluetooth OBEX Object Push Profile - CODE

I keep getting asked for the code for this article:

http://geoffwebbercross.blogspot.co.uk/2012/12/pushing-file-from-wp8-using-bluetooth.html

So here's the View Model example:

using BluetoothApp.Commanding;
using BluetoothApp.Helpers;
using BluetoothApp.Services.Interfaces;
using GalaSoft.MvvmLight;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
using WebberCross.Obelisk;
using Windows.Networking.Proximity;
using Windows.Networking.Sockets;
using Windows.Storage.Streams;

namespace BluetoothApp.ViewModel
{
    public class BluetoothShareVM : ViewModelBase
    {
        const string FILE_NAME = "demo.jpg";

        private readonly INavigationService _navService = null;
        private readonly ITaskService _taskService = null;
        private readonly IDialogService _diaService = null;
        private IReadOnlyList<PeerInformation> _pairedDevices = null;
        private StreamSocket _stream = null;
        private DataWriter _dataWriter = null;
        private DataReader _dataReader = null;

        private byte[] _shareData = null;
        private Brush _thumbnail = null;
        private DelegateCommand _shareCommand = null;
        private bool _isLoading = false;
        private bool _isShareExecutable = true;
        private string _message = Resources.StringTable.BTSearching;
        private bool _isShareVisible = false;
        private PeerInformation _selectedDevice = null;
        private int _progress = 0;
        private string _shareStatus = Resources.StringTable.BTSending;

        public string BTShareTitle { get { return Resources.StringTable.BTShareTitle; } }
        public string BTShareHeading { get { return Resources.StringTable.BTShareHeading; } }
        public string ShareText { get { return Resources.StringTable.ShareText; } }
        public string LogoutText { get { return Resources.StringTable.LogoutText; } }
        public string CheckPeersWarning { get { return Resources.StringTable.CheckPeersWarning; } }
        public string DevicesText { get { return Resources.StringTable.DevicesText; } }
        public string StatusText { get { return Resources.StringTable.StatusText; } }

        private List<byte[]> _packets = null;

        public byte[] ShareData
        {
            get { return this._shareData; }
            set
            {
                if (this._shareData != value)
                {
                    this._shareData = value;

                    base.RaisePropertyChanged("ShareData");
                }
            }
        }

        public string Message
        {
            get { return this._message; }
            set
            {
                if (this._message != value)
                {
                    this._message = value;

                    base.RaisePropertyChanged("Message");
                }
            }
        }

        public string ShareStatus
        {
            get { return this._shareStatus; }
            set
            {
                if (this._shareStatus != value)
                {
                    this._shareStatus = value;

                    base.RaisePropertyChanged("ShareStatus");
                }
            }
        }

        public int Progress
        {
            get { return this._progress; }
            set
            {
                if (this._progress != value)
                {
                    this._progress = value;

                    base.RaisePropertyChanged("Progress");
                }
            }
        }

        public Brush Thumbnail
        {
            get
            {
                if (this._thumbnail == null && this._shareData != null)
                {
                    this._thumbnail = Imaging.GetImageBrush(this._shareData, 0);
                }

                return _thumbnail;
            }
        }

        public bool IsLoading
        {
            get { return this._isLoading; }
            set
            {
                if (this._isLoading != value)
                {
                    this._isLoading = value;

                    base.RaisePropertyChanged("IsLoading");
                }
            }
        }

        public bool IsShareVisible
        {
            get { return this._isShareVisible; }
            set
            {
                if (this._isShareVisible != value)
                {
                    this._isShareVisible = value;

                    base.RaisePropertyChanged("IsShareVisible");
                }
            }
        }

        public DelegateCommand ShareCommand
        {
            get { return this._shareCommand; }
        }

        public bool IsShareExecutable
        {
            get { return this._isShareExecutable; }
            set
            {
                if (this._isShareExecutable != value)
                {
                    this._isShareExecutable = value;

                    base.RaisePropertyChanged("IsShareExecutable");
                    this._shareCommand.RaiseCanExecuteChanged();
                }
            }
        }

        public IReadOnlyList<PeerInformation> PairedDevices
        {
            get { return this._pairedDevices; }
            set
            {
                if (this._pairedDevices != value)
                {
                    this._pairedDevices = value;

                    base.RaisePropertyChanged("PairedDevices");
                }
            }
        }

        public PeerInformation SelectedDevice
        {
            get { return this._selectedDevice; }
            set
            {
                this.IsShareExecutable = value != null;

                if (this._selectedDevice != value)
                {
                    this._selectedDevice = value;

                    base.RaisePropertyChanged("SelectedDevice");
                }
            }
        }

        /// <summary>
        /// Initializes a new instance of the FacebookShareViewModel class.
        /// </summary>
        public BluetoothShareVM(INavigationService navService, ITaskService taskService, IDialogService diaService)
        {
            if (IsInDesignMode)
            {
                // Code runs in Blend --> create design time data.
            }
            else
            {
                // Code runs "for real": Connect to service, etc...
                this._navService = navService;
                this._taskService = taskService;
                this._diaService = diaService;

                this.InitialiseCommands();

                this.Search();
            }
        }

        private void InitialiseCommands()
        {
            // Background
            this._shareCommand = new DelegateCommand((param) =>
            {
                this.Share();
            },
                (p) =>
                {
                    return this.IsShareExecutable;
                });
        }

        private async void Search()
        {
            this.IsLoading = true;
            this.IsShareExecutable = false;

            this.Message = Resources.StringTable.BTSearching;

            // Note: You can only browse and connect to paired devices!
            // Configure PeerFinder to search for all paired devices.
            PeerFinder.AlternateIdentities["Bluetooth:Paired"] = "";

            try
            {
                this.PairedDevices = await PeerFinder.FindAllPeersAsync();
            }
            catch (Exception)
            {
               
            }

            this.IsLoading = false;
        }
       
        private async Task Share()
        {
            this.IsShareVisible = true;
            this.IsShareExecutable = false;
            this.ShareStatus = Resources.StringTable.BTSending;

            this.CreatePackets();

            // Select a paired device. In this example, just pick the first one.
            // Attempt a connection
            _stream = new StreamSocket();

            try
            {
                // Make sure ID_CAP_NETWORKING is enabled in your WMAppManifest.xml, or the next
                // line will throw an Access Denied exception.
                string oopUUID = "{00001105-0000-1000-8000-00805f9b34fb}";

                await _stream.ConnectAsync(_selectedDevice.HostName, oopUUID);
                _dataWriter = new DataWriter(_stream.OutputStream);
                _dataReader = new DataReader(_stream.InputStream);

                // Send data
                int maxServerPacket = await this.ObexConnect();

                if (maxServerPacket > 0)
                {
                    if (await ObexPushRequest())
                    {
                        // Success
                        this.ShareStatus = Resources.StringTable.BTSuccess;
                    }
                    else
                    {
                        // Failed
                        this.ShareStatus = Resources.StringTable.BTFailed;
                    }

                    this.Progress = 100;
                }

                this.ObexDisconnect();
            }
            catch (Exception ex)
            {
                this.ShareStatus = Resources.StringTable.BTFailed;
            }
        }



        private async Task<int> ObexConnect()
        {
            //send client request
            byte[] ConnectPacket = new byte[7];

            ConnectPacket[0] = 0x80;                            // Connect
            ConnectPacket[1] = (7 & 0xFF00) >> 8;       // Packetlength Hi Byte
            ConnectPacket[2] = (7 & 0xFF);                          // Packetlength Lo Byte
            ConnectPacket[3] = 0x10;                            // Obex v1
            ConnectPacket[4] = 0x00;                            // No flags
            ConnectPacket[5] = (2048 & 0xFF00) >> 8;    // 2048 byte client max packet size Hi Byte
            ConnectPacket[6] = (2048 & 0xFF);                   // 2048 byte max packet size Lo Byte

            _dataWriter.WriteBytes(ConnectPacket);
            await _dataWriter.StoreAsync();

            // Get response code
            await _dataReader.LoadAsync(1);
            byte[] buffer = new byte[1];
            _dataReader.ReadBytes(buffer);

            if (buffer[0] == 0xA0) // Sucess
            {
                // Get length
                await _dataReader.LoadAsync(2);
                buffer = new byte[2];
                _dataReader.ReadBytes(buffer);

                int length = buffer[0] << 8;
                length += buffer[1];

                // Get rest of packet
                await _dataReader.LoadAsync((uint)length - 3);
                buffer = new byte[length - 3];
                _dataReader.ReadBytes(buffer);

                int obexVersion = buffer[0];
                int flags = buffer[1];
                int maxServerPacket = buffer[2] << 8 + buffer[3];

                return maxServerPacket;
            }
            else
            {
                return -1;
            }
        }

        private async void ObexDisconnect()
        {
            byte[] bytes = new byte[3];
            bytes[0] = 0x81;
            bytes[1] = 0;
            bytes[2] = 3;

            _dataWriter.WriteBytes(bytes);
            await _dataWriter.StoreAsync();

            await _dataReader.LoadAsync(3);
            byte[] response = new byte[3];
            _dataReader.ReadBytes(response);

            _stream.Dispose();
            _stream = null;

            _dataReader.Dispose();
            _dataWriter.Dispose();
        }

        private async Task<bool> ObexPushRequest()
        {
            int step = 100 / this._packets.Count;

            foreach (var packet in this._packets)
            {
                this.Progress += step;

                _dataWriter.WriteBytes(packet);
                await _dataWriter.StoreAsync();

                // Get response code
                await _dataReader.LoadAsync(3);
                byte[] buffer = new byte[3];
                _dataReader.ReadBytes(buffer);

                // If not success and not continue it's an error
                if (buffer[0] != 0xA0 && buffer[0] != 0x90)
                    return false;
                else if (buffer[0] == 0xA0) // Success
                    return true;
            }

            return false;
        }


        private void CreatePackets()
        {
            int bodyLength = 1024;
            this._packets = new List<byte[]>();           

            // Chop data into packets           
            int blocks = (int)Math.Ceiling((decimal)this._shareData.Length / bodyLength);

            System.Text.UnicodeEncoding encoding = new System.Text.UnicodeEncoding(true, false);
            byte[] encodedName = encoding.GetBytes(FILE_NAME + new char());

            for (int i = 0; i < blocks; i++)
            {
                int headerLength = i == 0 ? 14 + encodedName.Length : 6;

                // Chop data into body
                byte[] body = null;
                if (i < blocks - 1)
                    body = new byte[bodyLength];
                else
                    body = new byte[this._shareData.Length - (i * bodyLength)];

                System.Buffer.BlockCopy(this._shareData, i * bodyLength, body, 0, body.Length);

                // Create packet
                byte[] packet = new byte[headerLength + body.Length];
                this._packets.Add(packet);

                // Build packet
                int offset = 0;
                packet[offset++] = i != blocks - 1 ? (byte)0x02 : (byte)0x82; // 0x02 for first blocks, 0x82 for last
                packet[offset++] = (byte)((packet.Length & 0xFF00) >> 8);
                packet[offset++] = (byte)(packet.Length & 0xFF);

                // Payload details on first packet
                if (i == 0)
                {
                    packet[offset++] = 0x01; // Name header
                    packet[offset++] = (byte)(((encodedName.Length + 3) & 0xFF00) >> 8);
                    packet[offset++] = (byte)((encodedName.Length + 3) & 0xFF);
                    System.Buffer.BlockCopy(encodedName, 0, packet, offset, encodedName.Length);
                    offset += encodedName.Length;
                    packet[offset++] = 0xC3; // Length header
                    packet[offset++] = (byte)((this._shareData.Length & 0xFF000000) >> 24);
                    packet[offset++] = (byte)((this._shareData.Length & 0xFF0000) >> 16);
                    packet[offset++] = (byte)((this._shareData.Length & 0xFF00) >> 8);
                    packet[offset++] = (byte)(this._shareData.Length & 0xFF);
                }

                packet[offset++] = 0x48; // Object body chunk header
                packet[offset++] = (byte)(((body.Length + 3) & 0xFF00) >> 8);
                packet[offset++] = (byte)((body.Length + 3) & 0xFF);
                System.Buffer.BlockCopy(body, 0, packet, offset, body.Length);
            }
        }

    }

}

And here's the View XAML:

<phone:PhoneApplicationPage x:Class="BluetoothApp.Pages.BluetoothShareView"
                            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                            xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
                            xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
                            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                            xmlns:preview="clr-namespace:Phone7.Fx.Preview;assembly=Phone7.Fx.Preview"
                            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                            xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit"
                            xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
                            xmlns:cmd="clr-namespace:BluetoothApp.Commanding"
                            mc:Ignorable="d"
                            d:DesignWidth="480"
                            d:DesignHeight="728"
                            FontFamily="{StaticResource PhoneFontFamilyNormal}"
                            FontSize="{StaticResource PhoneFontSizeNormal}"
                            Foreground="{StaticResource PhoneForegroundBrush}"
                            SupportedOrientations="Portrait"
                            Orientation="Portrait"
                            shell:SystemTray.IsVisible="False"
                            DataContext="{Binding Bluetooth, Source={StaticResource Locator}}">

    <toolkit:TransitionService.NavigationInTransition>
        <toolkit:NavigationInTransition>
            <toolkit:NavigationInTransition.Backward>
                <toolkit:TurnstileTransition Mode="BackwardIn"/>
            </toolkit:NavigationInTransition.Backward>
            <toolkit:NavigationInTransition.Forward>
                <toolkit:TurnstileTransition Mode="ForwardIn"/>
            </toolkit:NavigationInTransition.Forward>
        </toolkit:NavigationInTransition>
    </toolkit:TransitionService.NavigationInTransition>
    <toolkit:TransitionService.NavigationOutTransition>
        <toolkit:NavigationOutTransition>
            <toolkit:NavigationOutTransition.Backward>
                <toolkit:TurnstileTransition Mode="BackwardOut"/>
            </toolkit:NavigationOutTransition.Backward>
            <toolkit:NavigationOutTransition.Forward>
                <toolkit:TurnstileTransition Mode="ForwardOut"/>
            </toolkit:NavigationOutTransition.Forward>
        </toolkit:NavigationOutTransition>
    </toolkit:TransitionService.NavigationOutTransition>

    <Grid x:Name="LayoutRoot" Background="{StaticResource BackgroundBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <toolkit:PerformanceProgressBar Margin="0,-110,0,0" Padding="0" IsIndeterminate="True" IsEnabled="{Binding Path=IsLoading}" Visibility="{Binding IsLoading, Converter={StaticResource visConverter}}" />
        <ProgressBar Margin="24,-110,24,0" Padding="0" Minimum="0" Maximum="100" Value="{Binding Progress}" Visibility="{Binding IsShareVisible, Converter={StaticResource visConverter}}" />

        <!--TitlePanel contains the name of the application and page title-->
        <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,0">
            <TextBlock Text="{Binding Path=BTShareTitle, Mode=OneTime, FallbackValue=TITLE}" Style="{StaticResource PhoneTextNormalStyle}" />
            <TextBlock Text="{Binding Path=BTShareHeading, Mode=OneTime, FallbackValue=heading}" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}" />
        </StackPanel>

        <!--ContentPanel - place additional content here-->
        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="24,0,24,0">
            <Grid.RowDefinitions>
                <RowDefinition Height="200" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>

            <Rectangle Fill="{Binding Path=Thumbnail, FallbackValue=Red}" Height="200" Width="120" Stretch="Fill" HorizontalAlignment="Center" />

            <Grid Grid.Row="1" Visibility="{Binding IsShareVisible, Converter={StaticResource visConverter}, ConverterParameter='invert'}">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="*" />
                </Grid.RowDefinitions>
                <TextBlock Margin="0,20,0,0" Text="{Binding CheckPeersWarning}" Style="{StaticResource PhoneTextNormalStyle}"
                           TextWrapping="Wrap"/>
                <TextBlock Margin="0,20,0,0" Grid.Row="1" Text="{Binding DevicesText}" Style="{StaticResource PhoneTextTitle2Style}" />
                <ListBox Grid.Row="2" ItemsSource="{Binding PairedDevices}" SelectedItem="{Binding SelectedDevice, Mode=TwoWay}"
                         DisplayMemberPath="DisplayName"  Margin="0,10,0,0" FontSize="{StaticResource PhoneFontSizeMediumLarge}">
                </ListBox>
            </Grid>

            <Grid Grid.Row="1" Visibility="{Binding IsShareVisible, Converter={StaticResource visConverter}}">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="*" />
                </Grid.RowDefinitions>
                <TextBlock Margin="0,20,0,0" Text="{Binding StatusText}" Style="{StaticResource PhoneTextTitle2Style}" />               
                <TextBlock Grid.Row="2" Margin="0,20,0,0" Text="{Binding ShareStatus}" Style="{StaticResource PhoneTextNormalStyle}"
                           TextWrapping="Wrap"/>
            </Grid>
        </Grid>

        <preview:BindableApplicationBar BackgroundColor="#FFD82735">
            <preview:BindableApplicationBarIconButton IsEnabled="{Binding Path=IsShareExecutable}" Command="{Binding Path=ShareCommand}" IconUri="/Images/send.png" Text="{Binding Path=ShareText, Mode=OneTime}" />
        </preview:BindableApplicationBar>
    </Grid>
</phone:PhoneApplicationPage>