Динамичность и активность. Часть 3. Push-уведомления и Windows Azure Mobile Services

В 1-й части этого цикла статей мы рассмотрели, что означает "активность" для пользователя и как приложения участвуют в создании таких возможностей взаимодействия. Во 2-й части мы изучили способы программирования и отладки веб-служб для поддержки периодических обновлений для динамических плиток. В 3-й части мы дополним свой рассказ описанием способов доставки обновлений плиток, всплывающих и необработанных уведомлений по требованию для конкретных клиентских устройств через службу push-уведомлений Windows (WNS), а также расскажем о том, как компонент Windows Azure Mobile Services упрощает весь этот процесс.

Push-уведомления

Периодические обновления, как было показано во 2-й части, инициируются со стороны клиента и предоставляют метод опросадля обновления плитки или индикатора событий. Push-уведомления отображаются, когда служба напрямую отправляет на устройство обновление, которое может быть связано с конкретным пользователем, приложением или даже вспомогательной плиткой.

В отличие от опроса push-уведомления могут появляться в любой момент с гораздо более высокой частотой. Однако помните, что Windows будет снижать объем трафика push-уведомлений на устройстве, когда оно работает от батареи, находится в режиме ожидания с подключением или когда трафик уведомлений становится избыточным. То есть нет гарантии, что все уведомления будут доставлены (особенно если устройство отключено).

Поэтому не следует думать, что с помощью push-уведомлений можно реализовать плитку с часами или другие гаджеты на базе плиток, обеспечивающие аналогичное разрешение по частоте или времени. Вместо этого подумайте о том, как можно использовать push-уведомления для подключения плиток и уведомлений к серверной службе с интересными и важными сведениями, которые могут побудить пользователя снова запустить ваше приложение.

Прежде чем углубиться в детали, важно отметить, что существует два разных вида push-уведомлений:

  1. XML-обновления, содержащие обновленные данные для плитки или индикатора событий или полезные данные всплывающего уведомления. Windows может обрабатывать такие push-уведомления и выдавать обновление или всплывающее уведомление от имени приложения. При необходимости приложение также может обрабатывать такие уведомления напрямую.
  2. Двоичные или необработанные уведомления, содержащие любые отправляемые службой данные. Такие уведомления должны обрабатываться кодом самого приложения, так как в противном случае Windows не сможет определить, что делать с этими данными. Дополнительную информацию, например об ограничении размера (5 КБ) и кодировке (base64), можно прочитать в статье Руководство и контрольный список для необработанных уведомлений.

В обоих случаях запущенное приложение (переднего плана) может напрямую обрабатывать push-уведомления с помощью класса PushNotificationChannel и его события PushNotificationReceived. Для полезных данных XML приложение может изменять содержимое, теги и т. п. перед локальной публикацией этих данных (или принятием решения об их пропуске). Для необработанных уведомлений приложение обрабатывает его содержимое, а затем принимает решение о выпуске уведомлений (если они нужны).

Если приложение приостановлено или не запущено, то для выполнения указанных действий можно использовать фоновую задачу. Приложение должно запросить и получить доступ к экрану блокировки, что позволяет запускать код приложения при получении уведомления.

После получения уведомления фоновая задача обычно выполняет одну или две операции. Во-первых, она может сохранить необходимую информацию из уведомления в локальных данных приложения, чтобы приложение могло извлечь ее после следующей активации или возобновления работы. Во-вторых, фоновая задача может выдавать локальные обновления плиток и индикаторов событий и/или всплывающие уведомления.

Чтобы лучше разобраться в устройстве необработанных уведомлений, рассмотрим обычное приложение электронной почты. Когда его серверная служба обнаруживает новые адресованные пользователю сообщения, она отправляет в службу push-уведомлений Windows (WNS) необработанное push-уведомление, содержащее несколько заголовков сообщений электронной почты. Для этого служба использует универсальный код ресурса (URI) канала, подключенный к определенному приложению на устройстве пользователя.

Затем WNS пытается принудительно отправить это уведомление клиенту. В случае успешной отправки Windows получает это уведомление и ищет приложение, связанное с соответствующим URI канала. Если не удается найти подходящее приложение, уведомление игнорируется. Если приложение найдено и запущено, Windows активирует событие PushNotificationReceived, в противном случае Windows ищет и вызывает доступную фоновую задачу для данного приложения.

В любом случае необработанное уведомление передается в некоторый код приложения, который обрабатывает эти данные, выдает обновление индикатора событий для плитки приложения, чтобы указать количество новых сообщений, а также выдает до пяти циклических обновлений плитки с заголовками сообщений. Кроме того, приложение может выдать всплывающие уведомления для каждого из полученных новых сообщений или хотя бы одно такое уведомление, указывающее на наличие новой почты.

В результате всплывающие уведомления сообщают пользователю о поступлении новой электронной почты, а плитка приложения на начальном экране быстро отображает подробности о полученных сообщениях.

Более подробную информацию об этих обработчиках событий на стороне клиента и фоновых задачах можно найти в примере необработанных уведомлений, в статье Поддержание производительности при работе приложения в фоновом режиме — фоновые задачи данного блога, а также в техническом документе Работа с сетью в фоновом режиме. А теперь нам необходимо подробнее рассмотреть работу на стороне службы.

Работа со службой push-уведомлений Windows (WNS)

Совместная работа Windows, приложений, служб и службы WNS позволяет предоставлять предназначенные для пользователя данные определенной плитке приложения (или обработчику всплывающих или необработанных уведомлений) на определенном устройстве конкретного пользователя. Ниже показана схема связей между всеми этими компонентами.

 

Блок-схема совместной работы Windows, приложений, служб и WNS для предоставления данных определенной плитке приложения

 

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

  1. Вы (разработчик) регистрируете приложение в Магазине Windows для использования push-уведомлений. При этом вы получаете идентификатор безопасности и секрет клиента, используемые службой для проверки подлинности при взаимодействии с WNS (в целях безопасности эти сведения не следует хранить на клиентских устройствах).
  2. В среде выполнения приложение запрашивает у Windows универсальный код ресурса (URI) канала WNS для каждой из его динамических плиток (основной и вспомогательной) или один такой URI для необработанных уведомлений. Кроме того, приложение должно обновлять эти URI канала каждые 30 дней, для чего может использоваться еще одна фоновая задача.
  3. Служба приложения предоставляет URI, через который приложение может отправлять эти URI канала вместе с любыми данными, описывающими их использование (например, расположение для обновлений прогноза погоды или учетная запись и действия конкретного пользователя). После получения URI-кодов канала и соответствующих данных служба сохраняет их для дальнейшего использования.
  4. Служба отслеживает свой серверный компонент на наличие изменений, касающихся конкретного сочетания пользователя, устройства, приложения и плитки. При обнаружении условия, активирующего уведомление для определенного канала, служба формирует контент этого уведомления (в формате XML или без обработки), проходит проверку подлинности в WNS с помощью идентификатора безопасности и секрета клиента, а затем отправляет это уведомление в WNS вместе с URI канала.

Давайте подробнее рассмотрим каждый из этих этапов. (Забегая вперед: если вы не любите иметь дело с HTTP-запросами, ниже будет показано, как Windows Azure Mobile Services значительно упрощают выполнение многих задач.)

Регистрация приложения в Магазине Windows

Чтобы получить идентификатор безопасности и секрет клиента для службы, ознакомьтесь со статьей Проверка подлинности с помощью службы push-уведомлений Windows (WNS) в Центре разработчиков для Windows. Идентификатор безопасности определяет ваше приложение для WNS, а с помощью секрета клиента ваша служба сообщает WNS, что ей разрешено отправлять уведомления для вашего приложения. Еще раз отметим, что эти данные должны храниться только в службе.

Обратите внимание, что "Этап 4: отправка учетных данных облачного сервера службе WNS" в процедуре проверки подлинности с помощью службы push-уведомлений Windows (WNS) выполняется только в том случае, если ваша служба отправляет push-уведомление. Мы вернемся к этому вопросу немного позднее, поскольку на данном этапе у вашей службы нет основного компонента, необходимого для отправки уведомления, а именно URI канала.

Получение и обновление URI-кодов канала

Клиентское приложение получает URI-коды в среде выполнения через объект Windows.Networking.PushNotifications.PushNotificationChannelManager. Этот объект имеет всего два метода:

  • createPushNotificationChannelForApplicationAsync: создает URI канала для основной плитки приложения, а также всплывающих уведомлений и необработанных уведомлений.
  • createPushNotificationChannelForSecondaryTileAsync: создает URI канала для конкретной вспомогательной плитки, указанной аргументом tileId.

Результатом обеих асинхронных операций является объект PushNotificationChannel. Этот объект содержит в своем свойстве Uri URI канала, а также значение ExpirationTime, указывающее предельный срок обновления этого канала. Метод Close завершает работу канала, если это требуется, и, что важнее, является событием PushNotificationReceived, которое активируется, когда приложение запущено на переднем плане и через данный канал поступает push-уведомление.

Время существования URI канала составляет 30 дней, после этого срока WNS отклоняет все запросы для данного канала. Таким образом, коду приложения требуется обновлять эти URI с помощью указанных выше методов create не реже одного раза в 30 дней и отправлять их свою в службу. Ниже приводится хорошая стратегия:

  • При первом запуске запросите URI канала и сохраните эту строку в свойстве Uri в локальных данных приложения. Поскольку URI-коды связаны с соответствующим устройством, не храните их в перемещаемых данных приложения.
  • При последующих запусках снова запрашивайте URI и сравнивайте его с сохраненным ранее. Если он отличается, отправьте его в службу либо отправьте его и позвольте службе заменить более старую версию, если это необходимо.
  • Кроме того, выполните предыдущее действие в обработчике Resuming приложения (см. статью Запуск, возобновление и многозадачность), так как возможно, что работа приложения была приостановлена на более чем 30 дней.
  • Если вы считаете, что приложение не будет запущено в течение 30 дней, реализуйте фоновую задачу с триггером обслуживания, выполняемую каждые несколько дней или каждую неделю. Дополнительную информацию можно найти в статье Поддержание производительности при работе приложения в фоновом режиме — фоновые задачи; в данном случае фоновая задача выполняет тот же код, который приложение использует для запроса канала и отправки его в службу.

Отправка URI-кодов канала в службу

Обычно каналы push-уведомлений работают с обновлениями для конкретных пользователей, такими как состояние электронной почты, мгновенные сообщения и другие личные сведения. Маловероятно, что службе потребуется транслировать одно и то же уведомление каждому пользователю или каждой плитке. Поэтому требуется сопоставить каждый URI канала с более конкретной информацией. Для приложения электронной почты важнейшим является идентификатор пользователя, так как именно он определяет проверяемую учетную запись. С другой стороны, вполне вероятно, что приложение прогноза погоды сопоставит каждый URI канала с определенной широтой и долготой, чтобы каждая плитка (основная или вспомогательная) соответствовала определенному расположению.

Затем приложение должно указать эти сведения при отправке URI канала в службу, которая должна сохранить их для дальнейшего использования.

При использовании удостоверения пользователя приложению лучше всего выполнить отдельную проверку подлинности этого пользователя в службе с помощью соответствующих учетных данных или поставщика OAuth, такого как Facebook, Twitter, Google или учетная запись Майкрософт (как мы увидим далее, для Windows Azure Mobile Services лучше всего подходит использование OAuth). Если по какой-либо причине сделать это нельзя, обязательно шифруйте все отправляемые в службу идентификаторы пользователя либо используйте для отправки протокол HTTPS.

В любом случае именно вы выбираете, как именно следует отправлять информацию в службу (в заголовках, в данных из основной части сообщения или в виде параметров URI службы). Описанное взаимодействие осуществляется непосредственно между приложением и его службой.

В качестве простого примера предположим, что у нас есть служба со страницей receiveuri.aspx (как показывается в следующем разделе), полный адрес которой указан в переменной url. Следующий код запрашивает у Windows URI основного канала для приложения и публикует его на этой странице через протокол HTTP. (Этот пример является упрощенной версией, полученной из примера push- и периодических уведомлений на стороне клиента, где переменная itemId, определенная в другом месте, используется для идентификации вспомогательных плиток; для данного примера также имеется код на языке C++, который здесь не приводится):

JavaScript:

 

 Windows.Networking.PushNotifications.PushNotificationChannelManager
    .createPushNotificationChannelForApplicationAsync()
    .done(function (channel) {
        //Typically save the channel URI to appdata here.
        WinJS.xhr({ type: "POST", url:url,
            headers: { "Content-Type": "application/x-www-form-urlencoded" },
            data: "channelUri=" + encodeURIComponent(channel.uri) 
                + "&itemId=" + encodeURIComponent(itemId)
        }).done(function (request) {
            //Typically update the channel URI in app data here.
        }, function (e) {
            //Error handler
        });
    });

           

C#:

 

 using Windows.Networking.PushNotifications;

PushNotificationChannel channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();
HttpWebRequest webRequest = (HttpWebRequest)HttpWebRequest.Create(url);
webRequest.Method = "POST";
webRequest.ContentType = "application/x-www-form-urlencoded";
byte[] channelUriInBytes = Encoding.UTF8.GetBytes("ChannelUri=" + WebUtility.UrlEncode(newChannel.Uri) + "&ItemId=" + WebUtility.UrlEncode(itemId));

Task<Stream> requestTask = webRequest.GetRequestStreamAsync();
using (Stream requestStream = requestTask.Result)
{
    requestStream.Write(channelUriInBytes, 0, channelUriInBytes.Length);
}

Следующий код ASP.NET представляет собой базовую реализацию страницы receiveuri.aspx, которая обрабатывает этот метод HTTP POST и обеспечивает получение действительного URI канала, пользователя и некоторого идентификатора для элемента.

Следует подчеркнуть, что приведена именно "базовая" версия кода, так как функция SaveChannel, как можно увидеть, просто записывает содержимое запроса в заданный текстовый файл, что исключает возможность масштабирования числа пользователей! В полнофункциональной службе для этой цели используется база данных, однако общая структура этих служб схожа.

 <%@ Page Language="C#" AutoEventWireup="true" %>

<script runat="server">

protected void Page_Load(object sender, EventArgs e)
{
    //Output page header
    Response.Write("<!DOCTYPE html>\n<head>\n<title>Register Channel URI</title>\n</head>\n<html>\n<body>");
    
    //If called with HTTP GET (as from a browser), just show a message.
    if (Request.HttpMethod == "GET")
    {
        Response.StatusCode = 400;
        Response.Write("<p>This page is set up to receive channel URIs from a push notification client app.</p>");
        Response.Write("</body></html>");
        return;
    }

    if (Request.HttpMethod != "POST") {
        Response.StatusCode = 400;
        Response.Status = "400 This page only accepts POSTs.";
        Response.Write("<p>This page only accepts POSTs.</p>");
        Response.Write("</body></html>");        
        return;
    }
    
    //Otherwise assume a POST and check for parameters    
    try
    {
        //channelUri and itemId are the values posted from the Push and Periodic Notifications Sample in the Windows 8 SDK
        if (Request.Params["channelUri"] != null && Request.Params["itemId"] != null)
        {
            // Obtain the values, along with the user string
            string uri = Request.Params["channelUri"];
            string itemId = Request.Params["itemId"];
            string user = Request.Params["LOGON_USER"];
                 
            //TODO: validate the parameters and return 400 if not.
            
            //Output in response
            Response.Write("<p>Saved channel data:</p><p>channelUri = " + uri + "<br/>" + "itemId = " + itemId + "user = " + user + "</p>");

            //The service should save the URI and itemId here, along with any other unique data from the app such as the user;
            SaveChannel(uri, itemId, user);

            Response.Write("</body></html>");
        }
    }
    catch (Exception ex)
    {
        Trace.Write(ex.Message);
        Response.StatusCode = 500;
        Response.StatusDescription = ex.Message; 
        Response.End();
    }
}
</script>

protected void SaveChannel(String uri, String itemId, String user)
{
    //Typically this would be saved to a database of some kind; to keep this demonstration very simple, we'll just use
    //the complete hack of writing the data to a file, paying no heed to overwriting previous data.
    
    //If running in the debugger on localhost, this will save to the project folder
    string saveLocation = Server.MapPath(".") + "\\" + "channeldata_aspx.txt";
    string data = uri + "~" + itemId + "~" + user;

    System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
    byte[] dataBytes = encoding.GetBytes(data);

    using (System.IO.FileStream fs = new System.IO.FileStream(saveLocation, System.IO.FileMode.Create))
    {
        fs.Write(dataBytes, 0, data.Length);
    }

    return;
}

Код этой службы приведен в главе 13 моей бесплатной электронной книги Programming Windows 8 Apps in HTML, CSS, and JavaScript (Программирование приложений для Windows 8 на HTML, CSS и JavaScript) , а именно в примере HelloTiles в дополнительных материалах. Он предназначен для работы с указанным ранее примером SDK на стороне клиента. Если запустить HelloTiles в отладчике (Visual Studio 2012 Ultimate или Visual Studio Express 2012 для Web) с включенным localhost, вы получите URL-адрес вида https://localhost:52568/HelloTiles/receiveuri.aspx, который можно вставить в пример SDK на стороне клиента. Когда этот пример совершает запрос по данному URL-адресу, вы можете остановить выполнение в любой точке останова службы и выполнять код в пошаговом режиме.

Отправка уведомления

В реальной службе выполняется непрерывный процесс, отслеживающий источники данных и при необходимости отправляющий push-уведомления отдельным пользователям. Для этого может использоваться несколько способов:

  • Запланированное задание может проверять предупреждения о погоде для определенного расположения в зависимости от частоты вывода таких предупреждений, скажем, каждые 15–30 минут, и реагировать на них выдачей push-уведомлений.
  • Другая служба, например внутренняя служба обмена сообщениями, может выполнить запрос для страницы на сервере при наличии нового сообщения. Затем эта страница может выдать соответствующее уведомление.
  • Пользователи могут взаимодействовать с веб-страницами на сервере, а их действия могут активировать отправку push-уведомлений в определенные каналы.

Другими словами, для push-уведомлений существует несколько триггеров, зависящих исключительно от характера внутренней службы или веб-сайта. В соответствии с целями данной статьи в том же примере службы HelloTiles из главы 13 книги Programming Windows 8 Apps in HTML, CSS, and JavaScript (Программирование приложений для Windows 8 на HTML, CSS и JavaScript) имеется страница с именем sendBadgeToWNS.aspx, которая отправляет push-уведомление каждый раз, когда вы открываете эту страницу в браузере, используя URI канала, сохраненный в текстовом файле. Реальная служба выполняет поиск в базе данных, чтобы получить URI-коды канала, вместо считывания сведений из файла, однако еще раз отметим, что общая структура служб очень похожа.

Страница ASP.NET:

 <%@ Page Language="C#" AutoEventWireup="true" CodeFile="sendBadgeToWNS.aspx.cs" Inherits="sendBadgeToWNS" %>

<!DOCTYPE html>

<html xmlns="https://www.w3.org/1999/xhtml">
<head runat="server">    
    <title>Send WNS Update</title>
</head>
<body>
    <p>Sending badge update to WNS</p>    
</body>
</html>

 

Код программной части на C#:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Net;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;

public partial class sendBadgeToWNS : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        //Load our data that was previously saved. A real service would do a database lookup here
        //with user- or tile-specific criteria.
        string loadLocation = Server.MapPath(".") + "\\" + "channeldata_aspx.txt
        byte[] dataBytes;
        
        using (System.IO.FileStream fs = new System.IO.FileStream(loadLocation, System.IO.FileMode.Open))
        {
            dataBytes = new byte[fs.Length];
            fs.Read(dataBytes, 0, dataBytes.Length);
        }

        if (dataBytes.Length == 0)
        {
            return;
        }
        
        System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();

        string data = encoding.GetString(dataBytes);
        string[] values = data.Split(new Char[] { '~' });
        string uri = values[0]; //Channel URI
        string secret = "9ttsZT0JgHAFveYahK1B6jQbvMOIWYbm";
        string sid = "ms-app://s-1-15-2-2676450768-845737348-110814325-22306146-1119600341-293560589-2707026538";
        
        //Create some simple XML for a badge update
        string xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>";
        xml += "<badge value='alert'/>";
                    
        PostToWns(secret, sid, uri, xml, "wns/badge");
    }
}

 

Здесь мы просто получаем URI канала, формируем полезные данные XML, а затем проходим проверку подлинности в службе WNS и выполняем запрос HTTP POST по этому URI канала. Два последних действия выполняются в функции PostToWns, взятой из краткого руководства по отправке push-уведомления в Центре разработчиков Windows. Я не привожу полный листинг кода, так как основная его часть отвечает за проверку подлинности в службе WNS через OAuth (https://login.live.com/accesstoken.srf) с использованием секрета клиента и идентификатора безопасности из Магазина Windows. Результатом этой проверки подлинности является маркер доступа, который затем включается в запрос HTTP POST, отправляемый по URI канала:

C#:

 public string PostToWns(string secret, string sid, string uri, string xml, string type = "wns/badge")
{
    try
    {
        // You should cache this access token
        var accessToken = GetAccessToken(secret, sid);

        byte[] contentInBytes = Encoding.UTF8.GetBytes(xml);

        // uri is the channel URI
        HttpWebRequest request = HttpWebRequest.Create(uri) as HttpWebRequest;
        request.Method = "POST";
        request.Headers.Add("X-WNS-Type", type);
        request.Headers.Add("Authorization", String.Format("Bearer {0}", accessToken.AccessToken));

        using (Stream requestStream = request.GetRequestStream())
            requestStream.Write(contentInBytes, 0, contentInBytes.Length);

        using (HttpWebResponse webResponse = (HttpWebResponse)request.GetResponse())
            return webResponse.StatusCode.ToString();
    }
    catch (WebException webException)
    {
        // Implements a maximum retry policy (omitted)
    }
}

В этом примере обратите внимание на то, что для заголовка X-WNS-Type в HTTP-запросе установлено значение wns/badge, а для заголовка Content-Type по умолчанию используется значение text/xml. Для плиток требуется тип wns/tile, для всплывающих уведомлений — wns/toast. Для необработанных уведомлений используйте тип wns/raw и установите для Content-Type значение application/octet-stream. Дополнительные сведения о заголовках см. в документации на странице Заголовки запроса и ответа службы push-уведомлений.

Сбои push-уведомлений

Конечно, такая отправка HTTP-запроса может работать не всегда, и существует несколько причин, по которым ответ службы WNS будет отличаться от кода 200 (успешное выполнение). Дополнительные сведения см. в разделе "Код ответа" статьи Заголовки запроса и ответа службы push-уведомлений; ниже перечислены наиболее распространенные ошибки и их причины:

  • URI канала является недопустимым (404 Не найдено) или имеет истекший срок действия (410 Потеряно). В этих случаях службе следует удалить URI канала из своей базы данных и больше не выполнять для него запросы.
  • Секрет клиента и идентификатор безопасности могут быть недопустимыми (401 Не санкционировано), либо может возникнуть несоответствие между идентификатором пакета приложения в манифесте и аналогичным идентификатором в Магазине Windows (403 Запрещено). Для обеспечения соответствия этих идентификаторов удобнее всего воспользоваться командой Store > Associate App with Store (Магазин > Сопоставить приложение с Магазином) в Visual Studio (в Visual Studio 2012 Ultimate эта команда находится в меню Project (Проект)).
  • Размер полезных данных необработанного уведомления превышает 5 КБ (413 Слишком большой объект запроса).
  • Клиент может быть отключен от сети, в этом случае служба WNS автоматически повторяет попытку, однако в конечном счете сообщает о сбое. Для уведомлений XML режим работы по умолчанию заключается в том, что push-уведомления кэшируются и предоставляются при повторном подключении клиента к сети. По умолчанию кэширование для необработанных уведомлений отключено. Чтобы изменить этот параметр, в запросе, отправляемом на URI канала, установите для заголовка X-WNS-Cache-Policy значение cache.

Для других ошибок (400 Ошибочный запрос) убедитесь, что полезные данные XML содержат текст в кодировке UTF-8 и что необработанные уведомления имеют кодировку base64, а для заголовка Content-Type установлено значение application/octet-stream. Кроме того, служба WNS может осуществлять регулирование доставки, так как вы просто пытаетесь отправить слишком много push-уведомлений в заданный интервал времени.

Необработанное push-уведомление может отклоняться и в том случае, если приложение отсутствует на экране блокировки и устройство находится в режиме ожидания с подключением. Поскольку в этом состоянии Windows блокирует необработанные уведомления для всех приложений, отсутствующих на экране блокировки (как и в то время, когда приложение не выполняется на переднем плане), служба WNS оптимизирует свою работу, удаляя те уведомления, которые все равно не будут доставлены.

Windows Azure Mobile Services

Теперь, когда мы разобрались с довольно сложными подробностями работы с push-уведомлениями (хотя и пропустили вопросы хранения), у вас может возникнуть вопрос: "А можно ли сделать это все проще?". Только представьте себе, что потребуется для управления тысячами или даже миллионами URI-кодов канала для большой и расширяемой базы данных клиента!

К счастью, такие вопросы возникают не впервые. Кроме сторонних решений, предлагаемых, например, Urban Airship, для упрощения описанных задач отлично подходит решение Windows Azure Mobile Services.

Windows Azure Mobile Services (сокращенно AMS) — это готовое решение (по сути, это набор конечных точек REST), подходящее для большинства рассматриваемых нами служб. "Мобильная служба" управляет базой данных от вашего имени и предоставляет функции библиотеки, упрощая отправку полезных данных в службу WNS. Вводные сведения о Windows Azure Mobile Service приведены в статье Добавление облака к приложению с помощью Windows Azure Mobile Services.

Для реализации push-уведомлений сначала получите клиентскую библиотеку из пакета SDK Windows Azure Mobile Services для Windows 8. После этого обратитесь к статье, посвященной началу работы с push-уведомлениями в Mobile Services (обратите внимание, что на эту тему имеется и статья для языка JavaScript), которая описывает, как Windows Azure Mobile Services помогает выполнить описанные выше требования:

  • Регистрация приложения в Магазине Windows. Когда вы получите из Магазина Windows секрет клиента и идентификатор безопасности для своего приложения, сохраните эти значения в конфигурации мобильной службы. См. раздел, посвященный регистрации приложения в Магазине Windows упомянутой выше статьи.
  • Получение и обновление URI-кодов канала. Запрос URI-кодов канала и управление ими в приложении является исключительно клиентской задачей и выполняется точно так же, как было описано ранее.
  • Отправка URI-кодов канала в службу. Этот шаг значительно упрощается благодаря использованию Windows Azure Mobile Services. Сначала создайте в мобильной службе таблицу базы данных (с помощью портала Windows Azure). После этого приложение может просто вставлять в эту таблицу записи с URI-кодами канала и другой важной информацией. Клиентская библиотека Windows Azure Mobile Services самостоятельно обрабатывает HTTP-запрос и даже обновляет запись на стороне клиента в соответствии с любыми изменениями, внесенными на сервере. Кроме того, Windows Azure Mobile Services может автоматически обрабатывать проверку подлинности с использованием пользовательской учетной записи Майкрософт или трех других поставщиков OAuth (Facebook, Twitter или Google), если вы зарегистрировали приложение в одном из них. См. статью, посвященную началу работы с проверкой подлинности в Mobile Services.
  • Отправка уведомления. В мобильной службе вы можете подключить скрипты (написанные на одном из вариантов JavaScript, известном как Node.js) к операциям базы данных, а также создать запланированные задания на JavaScript. В таких скриптах простой вызов объекта push.wns с URI канала и полезными данными приводит к созданию требуемого HTTP-запроса в канал. Кроме того, упрощается отслеживание сбоев push-уведомлений и регистрация ответов с использованием console.log. Эти журналы можно легко просмотреть на портале Windows Azure.

Более подробные данные см. в двух следующих учебниках: Tile, Toast, and Badge Push Notifications using Windows Azure Mobile Services (Push-уведомления для плиток, всплывающих уведомлений и индикаторов событий с использованием Windows Azure Mobile Services) и Raw Notifications using Windows Azure Mobile Services (Необработанные уведомления с использованием Windows Azure Mobile Services). А сейчас вместо повторения приведенных в этих учебниках инструкций давайте рассмотрим несколько важных аспектов.

При настройке мобильная служба получает конкретный URL-адрес службы. Именно его вы используете при создании экземпляра объекта MobileServiceClient в пакете SDK Windows Azure Mobile Services:

JavaScript:

 var mobileService = new Microsoft.WindowsAzure.MobileServices.MobileServiceClient(
    "https://{mobile-service-url}.azure-mobile.net/",
    "{mobile-service-key}");

C#:

 using Microsoft.WindowsAzure.MobileServices;

MobileServiceClient MobileService = new MobileServiceClient(
    "https://{mobile-service-url}.azure-mobile.net/",
    "{mobile-service-key}");

C++ (взято из дополнительного примера):

 using namespace Microsoft::WindowsAzure::MobileServices;

auto MobileService = ref new MobileServiceClient(
    ref new Uri(L" https://{mobile-service-url}.azure-mobile.net/"), 
    ref new String(L"{mobile-service-key}"));

Этот класс инкапсулирует все HTTP-взаимодействие со службой, избавляя вас от утомительной работы с низкоуровневыми каналами, о которой и думать-то не хочется.

Чтобы пройти проверку подлинности у определенного поставщика OAuth, используйте метод login или LoginAsync, результатом которого является объект User, предоставляющий приложению соответствующую информацию. (После прохождения проверки подлинности свойство CurrentUser клиентского объекта будет также содержать идентификатор пользователя.) После такой прямой проверки подлинности мобильная служба получает доступ к идентификатору пользователя, поэтому клиенту не требуется отправлять его явным образом:

JavaScript:

 mobileService.login("facebook").done(function (user) { /* ... */ });
mobileService.login("twitter").done(function (user) { /* ... */ });
mobileService.login("google").done(function (user) { /* ... */ });
mobileService.login("microsoftaccount").done(function (user) { /* ... */ });

C#:

 MobileServiceUser user = await MobileService.LoginAsync(MobileServiceAuthenticationProvider.Facebook);
MobileServiceUser user = await MobileService.LoginAsync(MobileServiceAuthenticationProvider.Twitter);
MobileServiceUser user = await MobileService.LoginAsync(MobileServiceAuthenticationProvider.Google);
MobileServiceUser user = await MobileService.LoginAsync(MobileServiceAuthenticationProvider.MicrosoftAccount);

C++:

 task<MobileServiceUser^> (MobileService->LoginAsync(MobileServiceAuthenticationProvider::Facebook))
    .then([this](MobileServiceUser^ user) { /* */ } );
task<MobileServiceUser^> (MobileService->LoginAsync(MobileServiceAuthenticationProvider::Twitter))
    .then([this](MobileServiceUser^ user) { /* */ } );
task<MobileServiceUser^> (MobileService->LoginAsync(MobileServiceAuthenticationProvider::Google))
    .then([this](MobileServiceUser^ user) { /* */ } );
task<MobileServiceUser^> (MobileService->LoginAsync(MobileServiceAuthenticationProvider::MicrosoftAccount))
    .then([this](MobileServiceUser^ user) { /* */ } );

Снова отмечу, что отправка URI канала в службу заключается в простом сохранении записи в базе данных службы, куда клиентский объект отправляет HTTP-запросы. Для этого просто запросите объект базы данных и вставьте запись, как показано ниже (примеры взяты из указанного ранее учебника по push-уведомлениям). В каждом фрагменте кода предполагается, что ch содержит объект PushNotificationChannel из API-интерфейсов WinRT. Вы также можете включить в объект channel любые другие свойства, например идентификатор вспомогательной плитки или другие сведения, определяющие назначение данного канала.

JavaScript:

 var channelTable = MobileServicesSample.mobileService.getTable('Channels');

var channel = {
    uri: ch.uri, 
    expirationTime: ch.expirationTime.
};

channelTable.insert(channel).done(function (item) {

    },
    function () {
        // Error on the insertion.
    });
}

C#:

 var channel = new Channel { Uri = ch.Uri, ExpirationTime = ch.ExpirationTime };
var channelTable = privateClient.GetTable<Channel>();

if (ApplicationData.Current.LocalSettings.Values["ChannelId"] == null)
{
    // Use try/catch block here to handle exceptions
    await channelTable.InsertAsync(channel);
}

C++:

 auto channel = ref new JsonObject();
channel->Insert(L"Uri", JsonValue::CreateStringValue(ch->Uri));
channel->Insert(L"ExpirationTime", JsonValue::CreateBooleanValue(ch->ExpirationTime));

auto table = MobileService->GetTable("Channel");
task<IJsonValue^> (table->InsertAsync(channel))
    .then([this, item](IJsonValue^ V) { /* ... */ });

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

Кроме того, если вы неправильно укажите имя базы данных в вызове GetTable/getTable, то не увидите никаких исключений, пока не попытаетесь вставить запись. Выявление такой ошибки может оказаться непростой задачей. Если вы считаете, что все сделано правильно, хотя код и не работает, проверьте это имя базы данных.

Напомню, что такие вставки на стороне клиента переносятся в службу в форме HTTP-запросов, однако даже на стороне службы это осуществляется незаметно для вас. Вместо получения и обработки запросов вы подключаете к каждой операции базы данных (insert, read, update и delete) специальные скрипты.

Они написаны в виде функций JavaScript с использованием тех же встроенных объектов и методов, которые доступны в Node.js (однако все они не имеют никакого отношения к клиентскому коду JavaScript в приложении). Каждая функция получает соответствующие параметры: insert и update получают новую запись, delete получает идентификатор элемента, а read получает запрос. Кроме того, все функции получают объект user, когда приложение выполняет проверку подлинности пользователя в мобильной службе, и объект request, позволяющий выполнить операцию и сформировать HTTP-ответ.

Самый простой (и используемый по умолчанию) скрипт insert просто выполняет запрос и вставляет запись item в неизменном виде:

 function insert(item, user, request) {
    request.execute();
}

Если вы хотите прикрепить к записи метку времени и идентификатор пользователя, достаточно добавить эти свойства в параметр item перед выполнением запроса:

 function insert(item, user, request) {
    item.user = user.userId;
    item.createdAt = new Date();
    request.execute();
}

Обратите внимание, что в этом скрипте изменения, внесенные в параметр item перед вставкой в базу данных, автоматически распространяются обратно в клиент. В приведенном выше коде объект channel будет содержать свойства user и createdAt после успешного выполнения вставки в клиент. Это очень удобно!

После выполнения request.execute скрипты службы также могут выполнять и другие действия, особенно в качестве ответа на успешное выполнение или сбой, однако я не буду здесь на них останавливаться. Дополнительные сведения см. в документации с примерами и инструкциями для серверных скриптов.

Вернемся к push-уведомлениям. Сохранение URI-кодов канала в таблице является всего одной из составляющих задачи, поскольку служба может как отправлять, так и не отправлять уведомления в ответ на данное событие. Вероятнее всего, что служба будет располагать другими таблицами с дополнительной информацией, а выполняемые с этими таблицами операции будут активировать отправку уведомлений для некоторого подмножества URI-кодов канала. В следующем разделе мы рассмотрим несколько примеров. В любом случае вы отправляете push-уведомление из скрипта с помощью объекта push.wns. Существует множество методов для отправки отдельных видов обновлений (включая необработанные), где плитки, всплывающие уведомления и индикаторы событий работают непосредственно через методы names, соответствующие доступным шаблонам. Например:

 push.wns.sendTileSquarePeekImageAndText02(channel.uri, {
    image1src: baseImageUrl + "image1.png",
    text1: "Notification Received",
    text2: item.text
}, {
    success: function (pushResponse) {
        console.log("Sent Tile Square:", pushResponse);
    },
    error: function (err) {
        console.log("Error sending tile:", err);
    }

});

push.wns.sendToastImageAndText02(channel.uri, {
    image1src: baseImageUrl + "image2.png",
    text1: "Notification Received",
    text2: item.text
}, {
    success: function (pushResponse) {
        console.log("Sent Toast:", pushResponse);
    }
});

push.wns.sendBadge(channel.uri, {
    value: value,
    text1: "Notification Received"
}, {
    success: function (pushResponse) {
        console.log("Sent Badge:", pushResponse);
    }
});

Функция console.log создает запись в журналах, которые можно просмотреть на портале Azure Mobile Services, и обычно требуется включить вызовы журнала в обработчики ошибок, как в приведенном выше примере для уведомления плитки.

Вы могли заметить, что каждый метод send* связан с определенным шаблоном, поэтому широкие и квадратные представления полезных данных для плиток должны отправляться в виде двух отдельных уведомлений. Помните о том, что почти во всех случаях оба размера следует отправлять совместно, так как пользователь управляет внешним видом плитки на начальном экране. При использовании привязанных к шаблону функций send из push.wns это означает выполнение двух последовательных вызовов, каждый из которых создает отдельное push-уведомление.

Чтобы объединить несколько обновлений, для чего требуется одновременно отправить несколько обновлений плиток с разными тегами или отправить несколько всплывающих уведомлений, используйте методы push.wns.sendTile и push.wns.sendToast. Например:

 var channel = '{channel_url}';

push.wns.sendTile(channel,
    { type: 'TileSquareText04', text1: 'Hello' },
    { type: 'TileWideText09', text1: 'Hello', text2: 'How are you?' },
    { client_id: '{your Package Security Identifier}', client_secret: '{your Client Secret}' },

    function (error, result) {
        // ...
    });

На более низком уровне push.wns.send позволяет очень точно задавать содержимое уведомлений, а push.wns.sendRaw используется здесь для необработанных уведомлений. Дополнительные сведения см. в документации по объекту push.wns.

Реальные сценарии с Windows Azure Mobile Services

Пример приложения в статье Push-уведомления для плиток, всплывающих уведомлений и индикаторов событий с использованием Windows Azure Mobile Services показывает, как отправлять push-уведомления в ответ на вставку нового сообщения в таблицу базы данных. Однако в результате получается, что приложение отправляет push-уведомление самому себе, что в общем случае не требуется (возможно, кроме отправки уведомлений в это же приложение на других устройствах пользователя).

Гораздо вероятнее, что служба будет отправлять push-уведомления в ответ на события, происходящие за пределами приложения или плитки, которые в конечном итоге и получат эти уведомления. Рассмотрим несколько сценариев:

  • Использование социальных сетей

Приложения могут требовать от социальной сети пользователя реализации различных возможностей, например отправки вызовов друзьям. Так, если один из пользователей устанавливает новый рекорд в игре, может потребоваться отправить друзьям этого пользователя, у которых тоже установлена данная игра, вызовы с помощью обновлений плитки или всплывающих уведомлений. Такая же ситуация может возникнуть и в приложении для фитнеса, когда публикуется новое лучшее время для выполнения определенного упражнения.

В этом случае приложение может вставить новую запись в соответствующую таблицу мобильной службы (Scores, BestTimes и т. д.). С помощью скрипта вставки служба отправляет в базу данных запрос для поиска подходящих друзей текущего пользователя, а затем отправляет уведомления на соответствующие URI-коды канала. Дополнительные критерии запроса описывают конкретный аспект игры, вид упражнения (для вспомогательных плиток) и т. п.

  • Обновления и предупреждения прогноза погоды

Обычно приложения прогноза погоды позволяют назначить расположение основной плитке приложения и создать вспомогательные плитки для дополнительных расположений. Для каждой из плиток важное значение имеет информация о широте и долготе такого расположения, которую приложение отправляет в службу вместе с каждым URI канала (вставляя ее в таблицу). Для активации обновления этого канала служба может использовать другой процесс (например, описанное ниже запланированное задание), который периодически запрашивает обновления и предупреждения в центральной службе прогноза погоды, а затем обрабатывает полученные ответы и вставляет соответствующие сообщение в таблицу предупреждений в мобильной службе. После этого скрипт вставки получает соответствующие URI-коды канала и отправляет обновления. Кроме того, если сама служба прогноза погоды позволяет вам зарегистрироваться для получения предупреждений и периодических обновлений, другая страница этой службы будет получать такие запросы (вероятнее всего, это будут HTTP PUT), обрабатывать их и вызывать мобильную службу для вставки записи, активируя таким образом обновление.

  • Обмен сообщениями

Обработка мгновенных сообщений во многом аналогична получению обновлений прогноза погоды. И здесь другой процесс отслеживает получение новых сообщений (например, периодически проверяя наличие входящих сообщений) или регистрируется в источнике сообщений для получения предупреждений о поступающих сообщениях. В любом случае получение нового сообщения активирует отправку push-уведомлений в соответствующие каналы. При этом URI-коды канала сопоставляются с плиткой пользователя для определенного типа обмена сообщениями. Весьма вероятно, что здесь вы воспользуетесь необработанными уведомлениями, так как ситуация аналогична сценарию электронной почты, который описан в начале данной статьи.

Следует отметить, что во всех этих сценариях можно обойтись и без вставки сведений в таблицу базы данных. Если в скрипте вставки не выполняется вызов request.execute, запись в базу данных не производится, но вы можете выполнять с помощью этого скрипта и другие задачи, например отправку уведомлений. Другими словами, не нужно заполнять базу данных записями, которые не будут использоваться в дальнейшем, ведь хранение данных подразумевает определенные затраты.

Отмечу также, что Azure Mobile Services позволяют планировать задания. Подробнее об этом можно прочитать в статье, посвященной планированию повторяющихся заданий в Mobile Services. Такие задания могут периодически получать данные из других служб, обрабатывать их и вставлять записи в базу данных службы, активируя push-уведомления. Как было отмечено в части 2 этого цикла статей, другие веб-страницы и мобильные приложения также могут вносить изменения в эту базу данных и активировать push-уведомления. Во всех этих случаях в мобильной службе будут выполняться скрипты базы данных.

Заключение

В рамках данного цикла статей мы рассмотрели, какое значение имеют для пользователей, разработчиков, приложений и служб такие факторы, как "динамичность и активность". Мы познакомились с возможностями обновления плиток, индикаторов событий и всплывающих уведомлений, способами настройки периодических уведомлений и использования фоновых задач в рамках данного процесса, а также со структурой служб, обеспечивающей обработку push-уведомлений и периодических уведомлений. Можно с уверенностью утверждать, что Windows Azure Mobile Services обеспечивает представление всего процесса работы с push-уведомлениями на более высоком уровне. Этот компонент помогает существенно повысить производительность труда и избавляет от многих трудностей, например от написания новых служб с нуля.

Крэйг Брокшмидт (Kraig Brockschmidt)

Руководитель программы, рабочая группа по экосистеме Windows

Автор книги Programming Windows 8 Apps in HTML, CSS, and JavaScript (Программирование приложений для Windows 8 на HTML, CSS и JavaScript)