Статьи

MVVM Friendly Tap + Send и помощник подключения Bluetooth для Windows Phone 8

Зачем?
В начале этого года я написал в блоге о  TtcSocketHelper , дружественном MVVM-объекте, который позволял вам соединять два устройства  Windows Phone  8 друг с другом с помощью сокета. Я сделал с ним две игры —  Pull the Rope  и  2 Phone Pong , стиль «один на один». игры, в которые играют на двух телефонах, обмен данными JSON для синхронизации. За последние 10 месяцев произошло много событий: на рынок вышли очень дешевые устройства, прежде всего  Nokia Lumia 520 . Это полноценный Windows Phone 8, но он очень дешевый и, как следствие, он не поддерживает NFC (и, следовательно, не нажмите + отправить). По-видимому, это не касается покупателей, так как согласно  AdDuplex Статистика взлетела, как летучая мышь из ада. Всего за шесть месяцев он занял почти 33% рынка Windows Phone 8. Другими словами, ограничивая свое приложение телефонами с поддержкой NFC, я отрезал себя от значительной — и растущей — части рынка.

PhonePairConnectionHelper — поддержка просмотра по Bluetooth.
Итак, я переписал TtcSocketHelper в PhonePairConnectionHelper — в основном того же класса, но теперь он поддерживает подключение телефонов с использованием tap + send, а также с использованием проверенного и проверенного способа сопряжения Bluetooth. С 21 октября   в магазине появился 2 Phone Pong 1.3.0 , гордо работающий на этом компоненте, и теперь в мою игру могут играть на 50% больше людей!

В этой статье я собираюсь сосредоточиться на подключении через Bluetooth, так как подключение через tap + send уже описано в моей статье с января 2013 года.

Начало объекта снова достаточно простое и на самом деле очень похоже на предыдущее воплощение:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using GalaSoft.MvvmLight.Messaging;
using Windows.Foundation;
using Windows.Networking.Proximity;
using Windows.Networking.Sockets;
using Windows.Storage.Streams;

namespace Wp7nl.Devices
{
  public class PhonePairConnectionHelper
  {
    private ConnectMethod connectMode;

    public PhonePairConnectionHelper()
    {
      Messenger.Default.Register<NavigationMessage>(this, ProcessNavigationMessage);
      PeerFinder.TriggeredConnectionStateChanged +=
        PeerFinderTriggeredConnectionStateChanged;
      PeerFinder.ConnectionRequested += PeerFinderConnectionRequested;
    }
  
    private void ProcessNavigationMessage(NavigationMessage message)
    {
      Debug.WriteLine("PhonePairConnectionHelper.ProcessNavigationMessage " + 
                       message.NavigationEvent.Uri);

      if (message.IsStartedByNfcRequest)
      {
        Start();
      }
    }
  }
}

Наиболее примечательным является то, что сейчас настройка выполняется в конструкторе. Класс все еще слушает NavigationMessage и, если так, он все еще вызывает метод start (это для маршрута tap + send, который все еще работает). PeerFinder настроен на прослушивание событий TriggeredConnecState (т. Е. Событий, которые происходят после нажатия + отправки), а  также  — и это важно — для простых запросов Bluetooth (событие ConnectionRequested). Метод запуска сейчас сильно изменился:

public void Start(ConnectMethod connectMethod = ConnectMethod.Tap, 
  string displayAdvertiseName = null)
{
  PeerFinder.Stop();
  connectMode = connectMethod;
  if (!string.IsNullOrEmpty(displayAdvertiseName))
  {
    PeerFinder.DisplayName = displayAdvertiseName;
  }
  PeerFinder.AllowBluetooth = true;
  PeerFinder.Start();

  // Enable browse
  if (connectMode == ConnectMethod.Browse)
  {
    PeerFinder.FindAllPeersAsync().AsTask().
      ContinueWith(p => FirePeersFound(p.Result));
  }
}

Перечисление ConnectMethod, как вы уже можете видеть из кода, позволяет указать, какой метод вы хотите использовать:

namespace Wp7nl.Devices
{
  public enum ConnectMethod
  {
    Tap,
    Browse
  }
}

Метод Start имеет необязательный параметр, который дает вам возможность выбрать метод подключения и при необходимости добавить отображаемое имя. Отображаемое имя применимо только для метода Bluetooth. Интересно то, что если вы  не  предоставите имя, как я сделал в примере, он будет просто использовать имя телефона, как вы его предоставили (давая ему имя через Проводник, если вы подключаетесь через USB-кабель к ваш компьютер) — сокращенно до 15 символов. Почему это только 15 символов — ваше предположение так же хорошо, как и мое.

Если вы вызываете этот метод без каких-либо параметров, он использует значения по умолчанию и попытается подключиться, используя tap + send, как и раньше.

Прежде чем выбрать метод, вы можете проверить, какие методы подключения поддерживает устройство, используя два вспомогательных свойства объекта:

public bool SupportsTap
{
  get
  {
    return (PeerFinder.SupportedDiscoveryTypes &
       PeerDiscoveryTypes.Triggered) == PeerDiscoveryTypes.Triggered;
  }
}

public bool SupportsBrowse
{
  get
  {
    return (PeerFinder.SupportedDiscoveryTypes &
      PeerDiscoveryTypes.Browse) == PeerDiscoveryTypes.Browse;
  }
}

Имейте в виду — помощник не настолько умен, что отказывается подключаться в неподдерживаемом режиме. И я не проверяю это в примере приложения. Это зависит от вашего приложения. Но в любом случае — отступай немного. Если вы попытаетесь подключиться через Bluetooth, компонент попытается найти все одноранговые узлы, то есть другие телефоны, на которых запущено то же приложение, с помощью FindAllPeersAsync, и после этого вызовет событие PeersFound:

private void FirePeersFound(IEnumerable<PeerInformation> args)
{
  if (PeersFound != null)
  {
    PeersFound(this, args);
  }
}

public event TypedEventHandler<object, IEnumerable<PeerInformation>> PeersFound;

Ваше приложение должно подписаться на это событие, а затем оно получает список объектов «PeerInformation». И PeerEvent имеет ряд свойств, но вам нужно иметь дело только с одним свойством DisplayName. Вы отображаете это в списке, раскрывающемся списке или в любом другом приложении, и после того, как пользователь сделал выбор, вы вызываете метод Connect:

public void Connect(PeerInformation peerInformation)
{
  DoConnect(peerInformation);
}

private void DoConnect(PeerInformation peerInformation)
{
  PeerFinder.ConnectAsync(peerInformation).AsTask().ContinueWith(p =>
  {
    socket = p.Result;
    StartListeningForMessages();
    PeerFinder.Stop();
    FireConnectionStatusChanged(TriggeredConnectState.Completed);
  });
}

И бум. У вас есть подключение к телефону, который вы выбрали. Единственное, чего сейчас не хватает, так это того, что телефон, к которому вы подключились,  еще не подключен к вам . Но когда вы установили соединение, PeerFinder запустил событие ConnectionRequested. Помните, мы настроили прослушиватель «PeerFinderConnectionRequested» для этого в конструкторе? Ну думаю , что часть полезной нагрузки аргументов этого метода события получает  является объектом PeerInformation а , содержащий информацию для подключения  задней  к телефону , который просто подключен к вам :-). Что делает подключение довольно простым:

private void PeerFinderConnectionRequested(object sender, 
                                           ConnectionRequestedEventArgs args)
{
  if (connectMode == ConnectMethod.Browse)
  {
    DoConnect(args.PeerInformation);
  }
}

И сделано. Мы знаем, что у нас есть двустороннее сокетное соединение с использованием Bluetooth, и мы можем вызвать метод SendMessage, как и раньше, и прослушать результат, подписавшись на событие MessageReceived. Единственное, что я нарушил в интерфейсе этого помощника по отношению к предшественнику, — это тип события ConnectionStatusChanged. Раньше это было

public event TypedEventHandler<object, 
             TriggeredConnectionStateChangedEventArgs>
             ConnectionStatusChanged; 

и сейчас

public event TypedEventHandler<object, TriggeredConnectState>
             ConnectionStatusChanged;

Так что, если вы подписываетесь на это событие, вам не нужно проверять args.State, а только само args. Это было необходимо, чтобы иметь возможность запускать события подключения из DoConnect.

Остальная часть класса почти идентична старому TtcSocketHelper, и я не буду повторяться здесь.

Чтобы использовать этот класс
Сначала, решите, хотите ли вы использовать маршрут Tap + Send или маршрут Bluetooth. Мои приложения проверяют свойства SupportsTap и SupportsBrowse. Если поддерживается NFC (то есть нажатие + отправка), я предлагаю оба варианта и предоставляю пользователям выбор способа подключения. Если не поддерживается, я предлагаю только Bluetooth.

Для маршрута NFC путь по-прежнему:

  • Убедитесь, что ваше приложение запускает  NavigationMessage, как описано здесь
  • Создайте новый PhonePairConnectionHelper.
  • Подписаться на его событие ConnectionStatusChanged
  • Подпишитесь на событие MessageReceived
  • Call Start
  • Подождите, пока не появится TriggeredConnectState.Completed
  • Вызовите SendMessage — и увидите, что они появляются в методе, подписанном на MessageReceived на другом телефоне.

Способ использования Bluetooth:

  • Создайте новый PhonePairConnectionHelper.
  • Подписаться на его событие ConnectionStatusChanged Подписаться на его событие MessageReceived
  • Подписаться на его PeersFoundevent
  • Вызовите Start  с ConnectMethod.Browse и отображаемым именем.
  • Подождите, пока не произойдет событие PeersFound
  • Выберите одноранговый узел для подключения в вашем приложении
  • Вызовите метод Connect с выбранным узлом
  • Подождите, пока не появится TriggeredConnectState.Completed
  • Вызовите SendMessage — и увидите, что они появляются в методе, подписанном на MessageReceived на другом телефоне.

Так что, в принципе, нет особой разницы 😉

Изменения в оригинальном образце
Самое интересное, что вам даже не нужно сильно менять существующее  демонстрационное решение . Я добавил в viewmodel следующий код:

#region Bluetooth stuff

private bool useBlueTooth;
public bool UseBlueTooth
{
  get { return useBlueTooth; }
  set
  {
    if (useBlueTooth != value)
    {
      useBlueTooth = value;
      RaisePropertyChanged(() => UseBlueTooth);
    }
  }
}

private PeerInformation selectedPeer;

public PeerInformation SelectedPeer
{
  get { return selectedPeer; }
  set
  {
    if (selectedPeer != value)
    {
      selectedPeer = value;
      RaisePropertyChanged(() => SelectedPeer);
    }
  }
}

public ObservableCollection<PeerInformation> Peers { get; private set; }

private void PeersFound(object sender, IEnumerable<PeerInformation> args)
{
  Deployment.Current.Dispatcher.BeginInvoke(() =>
  {
    Peers.Clear();
    args.ForEach(Peers.Add);
    if (Peers.Count > 0)
    {
      SelectedPeer = Peers.First();
    }
    else
    {
      ConnectMessages.Add("No contacts found");
    }
  });
}

public ICommand ConnectBluetoothContactCommand
{
  get
  {
    return new RelayCommand(() =>
    {
      connectHelper.Connect(SelectedPeer);
      Peers.Clear();

    });
  }
}

#endregion

В принципе:

  • Свойство, чтобы определить, используете ли вы Bluetooth или нет (не = нажмите + отправить)
  • Собственность, держащая выбранного пира
  • Список доступных пиров
  • Обратный вызов для события помощника PeersFound. Не то, чтобы все здесь происходило в Диспетчере. Поскольку событие PeersFound возвращается из фонового потока, оно не имеет доступа к пользовательскому интерфейсу, но, поскольку оно обновляет несколько связанных свойств, ему необходим такой доступ — отсюда и Dispatcher.
  • и команда, позволяющая пользователю выбрать определенного партнера. Обратите внимание на Peers.Clear после того, как Peer выбран. Это потому, что я использую свой  HideWhenCollectionEmptyBehavior  для отображения пользовательского интерфейса для отображения и выбора пиров. Это поведение в примере решения в виде кода, так как я забыл добавить его в последний выпуск моей  библиотеки wp7nl на codeplex . * Ahem *

Два небольших дополнения к остальной части модели представления: в методе Init, где я настроил все connectHelper, теперь он говорит:

connectHelper = new PhonePairConnectionHelper();
connectHelper.ConnectionStatusChanged += ConnectionStatusChanged;
connectHelper.MessageReceived += TtsHelperMessageReceived;
connectHelper.PeersFound += PeersFound; // Added for Bluetooth support

И есть также небольшое изменение в StartCommand, чтобы учесть тот факт, что если пользователь выбирает Bluetooth, connectHelper должен вызываться немного по-другому.

public ICommand StartCommmand
{
  get
  {
    return new RelayCommand(
        () =>
        {
          ConnectMessages.Add("Connect started...");
          CanSend = false;
          CanInitiateConnect = false;
          // Changed for Bluetooth.
if(UseBlueTooth)
{
connectHelper.Start(ConnectMethod.Browse);
}
else
{
connectHelper.Start();
}
        });
  }
}

Чтобы пользователь мог фактически выбрать способ подключения, начальный экран немного изменился и теперь содержит две кнопки-переключателя. Bluetooth выбран по умолчанию.

<RadioButton Content="tap+send" 
IsChecked="{Binding UseBlueTooth, Converter={StaticResource BooleanInvertConvertor}, Mode=TwoWay}"  
  IsEnabled="{Binding CanInitiateConnect, Mode=OneWay}" />
<RadioButton Content="bluetooth" IsChecked="{Binding UseBlueTooth, Mode=TwoWay}" 
  HorizontalAlignment="Right" Margin="0" 
  IsEnabled="{Binding CanInitiateConnect, Mode=OneWay}" />
which is not quite rocket science, and some more code to give a user a simple UI to view and select found peers:

<Grid x:Name="bluetoothconnectgrid" VerticalAlignment="Bottom">
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
  </Grid.RowDefinitions>
  <i:Interaction.Behaviors>
    <behaviors:HideWhenCollectionEmptyBehavior Collection="{Binding Peers}"/>
  </i:Interaction.Behaviors>
  <toolkit:ListPicker x:Name="OpponentsListPicker" Margin="12,0" VerticalAlignment="Top" 
      ExpansionMode="FullScreenOnly" 
      ItemsSource="{Binding Peers}" SelectedItem="{Binding SelectedPeer, Mode=TwoWay}" />
  <Button Content="select contact" Height="72" Grid.Row="1" 
      Command="{Binding ConnectBluetoothContactCommand, Mode=OneWay}"/>
</Grid>

Также не очень сложный — средство выбора списка отображает одноранговые узлы и выбирает одноранговый узел, а кнопка запускает команду для фактического соединения. Мне скорее нравится использование HideWhenCollectionEmptyBehavior здесь — это автоматически показывает этот кусок пользовательского интерфейса, поскольку в списке есть одноранговые узлы, и скрывает его, когда их нет. Это довольно элегантно, если можно так выразиться. На картинке справа вы можете увидеть название моего 920 — сокращенно до 15 символов.

После того, как соединение было установлено — либо через Bluetooth, либо нажатием + отправить — вы можете использовать это приложение для чата, как и в предыдущей версии.

Важные вещи, которые нужно знать

  • Если вы разработчик, который хочет защитить своих клиентов от разочарований, вы отметили «NFC» как требование в файле WMAppManifest.xml, когда создавали приложение поверх моего предыдущего TtcSocketHelper. Уберите галочку прямо сейчас . Или же ваше приложение все еще не будет доступно для телефонов, таких как 520, которые не имеют NFC. И это совершенно не соответствует сути всего этого нового класса.
  • Я обнаружил, что при просмотре Bluetooth в Windows Phone 8 необходимо учитывать некоторые особенности
    • Вы все еще должны быть достаточно близко друг к другу — где-то в пределах метра — чтобы найти пиров. Как только соединение установлено, вы можете двигаться дальше друг от друга
    • Первый телефон, который начинает поиск пира, обычно ничего не находит. Второй находит одного противника, третий — двух. Процесс поиска обычно не длится очень долго — в большинстве случаев всего несколько секунд — до того, как PeerFinder либо сдается, либо возвращается с одноранговым узлом. Желательно не вызывать Connect автоматически, если вы нашли только одного пира — это может быть не тот, кого искал пользователь. Всегда держите пользователя под контролем.
  • Если вы подключаетесь через касание + отправка, вам нужно будет только запустить приложение на  одном  из телефонов и нажать кнопку подключения — другой телефон автоматически запустит приложение (или даже загрузит его, если его там нет), когда устройства связаны между собой.
  • Для   маршрута Bluetooth приложение должно быть запущено на  обоих  телефонах, и на  обоих  телефонах вам нужно будет нажать кнопку подключения. Заключение
    Рынок приложений постоянно меняется, это справедливо для всех платформ, но это точно относится и к Windows Phone. Важно быть в курсе этих событий. На рынке Windows Phone наблюдается значительный рост, особенно на нижних моделях телефонов. Важно пройти лишнюю милю для этих групп — обслуживайте как можно больше пользователей. Надеюсь, я немного помог вам с этим, как на общем примере, так и на реальном коде.

Демо-решение, как всегда,  можно скачать здесь .