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>

Friday, 18 October 2013

Launching an Email Client from Windows 8 Store Apps – Language Comparison

Unlike in Windows Phone apps, there is not a dedicated task for composing email, instead, a general purpose launcher is used for launching URIs. To launch an email client, a mailto: type URI can be used. The launching itself is pretty much identical in C#, VB.Net, C++ and JavaScript; however the text needs escaping to cope with spaces, carriage returns etc and this is where the languages differ:

C#
In .Net for C# and VB.Net, the Uri class has a static method ‘EscapeUriString’ which takes care of escaping URI strings:

var email = string.Format(“mailto:{0}?subject={1}&body={2}”,
            “email@somedomain.com",
            "A Subject",
            “The email body”);

var cleaned = Uri.EscapeUriString(email);

// Email task
await Launcher.LaunchUriAsync(new Uri(cleaned, UriKind.Absolute));

VB.Net

Dim email = String.Format(SUPPORT_MAIL,
            “email@somedomain.com",
            "A Subject",
            “The email body”)

Dim cleaned = Uri.EscapeUriString(email)

            ' Email task
Await Launcher.LaunchUriAsync(New Uri(cleaned, UriKind.Absolute))

JavaScript
WinRT JavaScript Uri object doesn’t have the ‘EscapeUriString’ method, however it supports the standard JavaScript encodeURI method which does the same thing:

var email = "mailto:";
            email += "email@somedomain.com";
            email += "?subject=";
            email += "A Subject";
            email += "&body=";
            email += "The email body";

var cleaned = encodeURI(email);

// Email task
var uri = new Windows.Foundation.Uri(cleaned);
return Windows.System.Launcher.launchUriAsync(uri);

C++
WinRT JavaScript Uri object doesn’t have the ‘EscapeUriString’ method either and doesn’t have anything build in, so we need to write one ourselves.

String^ email = L"mailto:";
       email += "email@somedomain.com";
       email += L"?subject=";
       email += "A Subject";
       email += L"&body=";
       email += "The email body";

auto cleaned = encodeURI(email);

auto uri = ref new Uri(cleaned);
Launcher::LaunchUriAsync(uri);

I found the main bit of this enclode function on Stack Overflow (can’t find where now) and modified it to work with WinRT:

String^ encodeURI(String^ c)
{
    const std::string unreserved = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!*()\\,/?:@=+$&";

    std::wstring  escaped=L"";
    for(size_t i=0; i<c->Length(); i++)
    {
       auto data = c->Data();

       if (unreserved.find_first_of(data[i]) != std::string::npos)
       {
           escaped.push_back(data[i]);
       }
       else
       {
           escaped.append(L"%");
           char buf[3];
           sprintf_s(buf, "%.2X", data[i]);

           auto s = std::string(buf);
           auto w = std::wstring(s.begin(), s.end());

           escaped.append(w);
       }
    }

    return ref new Platform::String(escaped.c_str());

}

Inside Windows Phone 64 - Rate my app

Cool to see a code sample I wrote for bolser and Nokia getting the spotlight!

http://blogs.windows.com/windows_phone/b/wpdev/archive/2013/10/17/inside-windows-phone-64-rate-my-app.aspx

Thursday, 5 September 2013

Digitally Signing a Google API V3 Service Request URL Using C#

Overview

If you have a Business account for increased volumes of traffic, you have to sign the request URL using your private key, rather than using an API key which was required for V2.

This article details what is required and has an example of how to implement signing using Python.

The thing which tripped me up was this:

“Note: Modified Base64 for URLs replaces the + and / characters of standard Base64 with - and _ respectively, so that these Base64 signatures no longer need to be URL-encoded.”

Our key had ‘_’ characters in which needed swapping to ‘/’ in order to convert to Base64 bytes and many of the generated hashes had ‘+’ and ‘/’ characters which needed converting to ‘-‘ and ‘_’.

String Formats

The following constants are the string formats I used for the full URL and the portion of the URL for signing:

private const string SEARCH_URL = "http://maps.googleapis.com/maps/api/geocode/json?latlng={0},{1}&sensor=false&client={2}&signature={3}";
private const string SIGN_URL = "/maps/api/geocode/json?latlng={0},{1}&sensor=false&client={2}";

CreateSignature Method

This method creates a signature for the URL with the private key:

private string CreateSignature(string url, string privateKey)
{    
  // Replace file-safe characters
  byte[] key = Convert.FromBase64String(privateKey.Replace('-', '+').Replace('_', '/'));
  byte[] buffer = Encoding.UTF8.GetBytes(url);
  byte[] hash = null;

  // Initialize the keyed hash object.
  using (HMACSHA1 hmac = new HMACSHA1(key))
  {
    // Create hash
    hash = hmac.ComputeHash(buffer);
  }

  // Replace unsafe characters
  return Convert.ToBase64String(hash).Replace('+', '-').Replace('/', '_');
}

Implementation

It can be implemented in the following example which returns an address for a lat/long position (Position and Address are basic POCO objects):

private string _privateKey = "My_Private_Key=";
private string _clientId = "My-ClientId";   

public async Task<Address> GetAddress(Position position)
{
  string url = string.Format(SIGN_URL, position.Latitude, position.Longitude, _clientId);

  string sig = this.CreateSignature(url, this._privateKey);

  string search = string.Format(SEARCH_URL, position.Latitude, position.Longitude, _clientId, sig);
  string data = null;

  try
  {
    var req = WebRequest.CreateHttp(search);
    var res = await req.GetResponseAsync();

    using (Stream s = res.GetResponseStream())
    using (StreamReader str = new StreamReader(s))
    {
      data = await str.ReadToEndAsync();
    }
  }
  catch (WebException wex)
  {
    throw new Exception("Web server request failed", wex);
  }

  if (data != null)
  {
    var address = this.ParseAddressJson(data);

    return address;
  }

  return null;
}

I’ve not included the JSON parsing routine, but that bit’s fairly easy to implement.

If something is wrong with the signature you will get a 403 code.

Thursday, 18 July 2013

WP7 & WP8 XAP Windows Phone App

I've recently been having problems with an app that has a WP7 and WP8 package.

At first the package had the same version when submitted and WP8 devices correctly got the WP8 XAP and WP7 devices got the WP7 XAP.

Suddenly with no change, WP8 devices started getting the WP7 XAP.

We put in a ticket with the store team and the mentioned the versions had to be different, so I updated the WP8 package higher than the WP7 one and submitted it, in accordance with this guideline:

http://blogs.windows.com/windows_phone/b/wpdev/archive/2013/01/15/app-submission-tips-for-windows-phone-7-and-windows-phone-8.aspx

This still did not help.

We asked support again and they examined the XAPs and started going on about one targeting 8.0 and one 7.1 AND 8.0. Well of course the 7 one will target both, because it's supposed to, but surely the store should be able to work it out?

We resubmitted a few times and nothing worked and we kept getting responses about the 7.1 and 8 thing.

Eventually I decided to up-version both XAPs with a minor fix together and that finally seemed to work!

Here's my check list for future reference:
  • Make sure the WP8 package has a higher version than the WP7 one in the WMAppManifest
  • Make sure the WP8 package has a higher version than the WP7 one on the store
  • Submit 2 XAPs at the same time

I've got another minor update to push out this week so will see if it works again!

Wednesday, 10 July 2013

Windows 8 MVVM Drag Drop

Overview

I’ve recently added drag/drop functionality to a Windows 8 app between a GridView and a ListBox using an MVVM pattern. I created two Attached Properties to collect the dragged items and fire a command when they were dropped. Both work on ListViewBase controls.

DragItemStarting Attached Property


This detects the DragItemsStarting event on the source control and pushes the items into a bound IList<T> in the view model:

public class DragItemsStarting
{
    public static readonly DependencyProperty ItemsProperty =
            DependencyProperty.RegisterAttached("Items", typeof(IList<object>), typeof(DragItemsStarting), new PropertyMetadata(null, ItemsPropertyChanged));

    public static void SetItems(DependencyObject attached, IList<object> value)
    {
        attached.SetValue(ItemsProperty, value);
    }

    public static IList<object> GetItems(DependencyObject attached)
    {
        return (IList<object>)attached.GetValue(ItemsProperty);
    }

    private static void ItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        // Attach click handler
        (d as ListViewBase).DragItemsStarting += DragItemstarting_DragItemsStarting;
    }
             
    static void DragItemstarting_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
    {
        // Set Items
        SetItems(sender as DependencyObject, e.Items);
    }
}

DragItem Binding XAML


<GridView
    x:Name="pallette"
    cmd:DragItemsStarting.Items="{Binding DragObjects, Mode=TwoWay}"

DragItem Binding View Model

public IList<object> DragObjects
{
    get { return this._dragObjects; }
    set { this._dragObjects= value; }
}

Be careful to implement the getter and not return null as the attached property won’t attach if the value doesn’t change.

DropCommand Attached Property

This detects the drop event and fires the bound command. In my implementation, I wanted the pointer location so I passed that into the command:

public class DropCommand
{
    public static readonly DependencyProperty CommandProperty =
            DependencyProperty.RegisterAttached("Command", typeof(ICommand), typeof(DropCommand), new PropertyMetadata(null, CommandPropertyChanged));

    public static void SetCommand(DependencyObject attached, ICommand value)
    {
        attached.SetValue(CommandProperty, value);
    }

    public static ICommand GetCommand(DependencyObject attached)
    {
        return (ICommand)attached.GetValue(CommandProperty);
    }

    private static void CommandPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        // Attach click handler
        (d as FrameworkElement).Drop += DropCommand_Drop;
    }

    private static void DropCommand_Drop(object sender, DragEventArgs e)
    {
        e.Handled = true;

        // Get element
        var fe = (sender as FrameworkElement);

        // Get command
        ICommand command = GetCommand(fe);

        // Get pointer point
        var p = Window.Current.CoreWindow.PointerPosition;

        Debug.WriteLine(string.Format("DROP X: {0}, Y: {1}", p.X, p.Y));

        // Execute command
        command.Execute(p);
    }
}

DropCommand Binding XAML


<ListBox cmd:DropCommand.Command="{Binding DropCommand}"

DropCommand Binding View Model


public ICommand DropCommand
{
    get { return this._dropCommand; }
}

I usually have a method called by the constructor that created the commands:

this._dropCommand = new DelegateCommand(a =>
    {
        if (a is Windows.Foundation.Point)
        {
            this.DoSomething((Windows.Foundation.Point)a);
        }
       
    }, p => this.IsDropExecutable);

You can use any type of command; I've used Delegate command taken from the Prism framework. 

Conclusion

This provides a nice clean way of implementing drag and drop using an MVVM pattern.