Статьи

Push-уведомления для Windows Phone 7 с использованием служб SQL Azure и Cloud — часть 1/3

В этой серии статей я напишу о push-уведомлениях для Windows Phone 7 с акцентом на стороне сервера, охватывающей управление подписками на уведомления, сопоставление подписок и планирование уведомлений. На стороне сервера будет использоваться SQL Azure и веб-служба WCF, развернутая в Azure.

В качестве примера я создам очень простое приложение для чтения новостей, где пользователь может подписаться на получение уведомлений, когда публикуется новая статья в данной категории. Push-уведомления будут доставлены как тост-уведомления клиенту. Сторона сервера будет состоять из базы данных SQL и веб-служб WCF, которые я развертываю в Windows Azure.

Создание базы данных SQL Azure

База данных будет хранить информацию о подписке и новостные статьи. Для этого я создам три таблицы; Категория, новости и подписка.

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

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

USE [PushNotification]
GO

CREATE TABLE [dbo].[Category]
(
[CategoryId] [int] IDENTITY (1,1) NOT NULL,
[Name] [nvarchar](60) NOT NULL,
CONSTRAINT [PK_Category] PRIMARY KEY ([CategoryId])
)
GO

CREATE TABLE [dbo].[Subscription]
(
[SubscriptionId] [int] IDENTITY (1,1) NOT NULL,
[DeviceId] [uniqueidentifier] NOT NULL,
[ChannelURI] [nvarchar](250) NOT NULL,
PRIMARY KEY ([SubscriptionId]),
[CategoryId] [int] NOT NULL FOREIGN KEY REFERENCES [dbo].[Category]([CategoryId])
)
GO

CREATE TABLE [dbo].[News]
(
[NewsId] [int] IDENTITY (1,1) NOT NULL,
[Header] [nvarchar](60) NOT NULL,
[Article] [nvarchar](250) NOT NULL,
[AddedDate] [datetime] NOT NULL,
PRIMARY KEY ([NewsId]),
[CategoryId] [int] NOT NULL FOREIGN KEY REFERENCES [dbo].[Category]([CategoryId])
)
GO

 
При запуске этого сценария создаются три таблицы Category, Subscription и News, которые будут использоваться в этом примере. Имейте в виду, что я не нормализовал таблицы, чтобы не усложнять код. Вы можете создать обычную базу данных MS SQL и запускать ее локально или на сервере, если хотите, но для этого примера я использовал SQL Azure.

Создание облачного сервиса WCF

Облачная служба WCF будет использоваться приложением WP7 и будет взаимодействовать с базой данных SQL Azure. Служба отвечает за отправку уведомлений клиентам WP7.

Действия по созданию облачного сервиса WCF с использованием объектной модели

Создайте проект Windows Cloud в Visual Studio 2010, я назвал мой NewsReaderCloudService . Выберите веб-роль службы WCF, я назвал мой NewsReaderWCFServiceWebRole . Затем вам нужно создать объектную модель, которая подключается к вашей базе данных SQL Azure, я назвал мою NewsReaderModel и назвал сущность NewsReaderEntities . Для более подробного объяснения того, как это сделать, вы можете взглянуть на статью, которую я написал ранее по этому вопросу.

Если вы выполнили эти шаги, у вас должен быть проект Windows Cloud, подключенный к вашей базе данных SQL Azure.

Создание контракта WCF

Контракт WCF записывается в автоматически сгенерированный файл IService.cs . Первым делом я переименую этот файл в INewsReaderService.cs . В этом интерфейсе я добавил семь методов OperationContract : SubscribeToNotifications, RemoveCategorySubscription, GetSubscribetion, AddNewsArticle, AddCategory, GetCategories и PushToastNotifications. Я объясню эти методы в следующем разделе, когда мы реализуем сервис на основе интерфейса INewsReaderService . Ниже вы можете увидеть код для INewsReaderService.cs

using System;
using System.Collections.Generic;
using System.ServiceModel;

namespace NewsReaderWCFServiceWebRole
{
[ServiceContract]
public interface INewsReaderService
{
[OperationContract]
void SubscribeToNotification(Guid deviceId, string channelURI, int categoryId);

[OperationContract]
void RemoveCategorySubscription(Guid deviceId, int categoryId);

[OperationContract]
List<int> GetSubscriptions(Guid deviceId);

[OperationContract]
void AddNewsArticle(string header, string article, int categoryId);

[OperationContract]
void AddCategory(string category);

[OperationContract]
List<Category> GetCategories();

[OperationContract]
void PushToastNotifications(string title, string message, int categoryId);
}
}

Creating the service

The service implements the INewsReaderService interface and this is done in the auto generated Service1.svc.cs file, the first thing I do is to rename it to NewsReaderService.svc.cs. I start with implementing members from the interface. Below you can see the code for NewsReaderService.svc.cs with empty implementations. In the next sections I will complete the empty methods implemented.

using System;
using System.Collections.Generic;
using System.Data.Objects;
using System.IO;
using System.Linq;
using System.Net;
using System.Xml;

namespace NewsReaderWCFServiceWebRole
{
public class NewsReaderService : INewsReaderService
{
public void SubscribeToNotification(Guid deviceId, string channelURI, int categoryId)
{
throw new NotImplementedException();
}

public void RemoveCategorySubscription(Guid deviceId, int categoryId)
{
throw new NotImplementedException();
}

public List<int> GetSubscriptions(Guid deviceId)
{
throw new NotImplementedException();
}

public void AddNewsArticle(string header, string article, int categoryId)
{
throw new NotImplementedException();
}

public void AddCategory(string category)
{
throw new NotImplementedException();
}

public List<Category> GetCategories()
{
throw new NotImplementedException();
}

public void PushToastNotifications(string title, string message, int categoryId)
{
throw new NotImplementedException();
}
}
}

SubscribeToNotification method

This is the method that the client calls to subscribe to notifications for a given category. The information is stored in the Subscription table and will be used when notifications are pushed.

public void SubscribeToNotification(Guid deviceId, string channelURI, int categoryId)
{
using (var context = new NewsReaderEntities())
{
context.AddToSubscription((new Subscription
{
DeviceId = deviceId,
ChannelURI = channelURI,
CategoryId = categoryId,
}));
context.SaveChanges();
}
}

RemoveCategorySubscription method

This is the method that the client call to remove a subscription for notifications for a given category. If the Subscription table has a match for the given device ID and category ID this entry will be deleted.

public void RemoveCategorySubscription(Guid deviceId, int categoryId)
{
using (var context = new NewsReaderEntities())
{
Subscription selectedSubscription = (from o in context.Subscription
where (o.DeviceId == deviceId && o.CategoryId == categoryId)
select o).First();
context.Subscription.DeleteObject(selectedSubscription);
context.SaveChanges();
}
}

GetSubscribtions method

 This method is used to return all categories which a device subscribes to.

public List<int> GetSubscriptions(Guid deviceId)
{
var categories = new List<int>();
using (var context = new NewsReaderEntities())
{
IQueryable<int> selectedSubscriptions = from o in context.Subscription
where o.DeviceId == deviceId
select o.CategoryId;
categories.AddRange(selectedSubscriptions.ToList());
}
return categories;
}

AddNewsArticle method

This method is a utility method so that we can add a new news article to the News table. I will use this later on to demonstrate push notification sent based on content matching. When a news article is published for a given category only the clients that have subscribed for that category will receive a notification.

public void AddNewsArticle(string header, string article, int categoryId)
{
using (var context = new NewsReaderEntities())
{
context.AddToNews((new News
{
Header = header,
Article = article,
CategoryId = categoryId,
AddedDate = DateTime.Now,
}));
context.SaveChanges();
}
}

AddCategory method

This method is also a utility method so that we can add new categories to the Category table.

public void AddCategory(string category)
{
using (var context = new NewsReaderEntities())
{
context.AddToCategory((new Category
{
Name = category,
}));
context.SaveChanges();
}
}

GetCategories method

This method is used by the client to get a list of all available categories. We use this list to let the user select which categories to receive notifications for.

public List<Category> GetCategories()
{
var categories = new List<Category>();
using (var context = new NewsReaderEntities())
{
IQueryable<Category> selectedCategories = from o in context.Category
select o;
foreach (Category selectedCategory in selectedCategories)
{
categories.Add(new Category {CategoryId = selectedCategory.CategoryId, Name = selectedCategory.Name});
}
}
return categories;
}

PushToastNotification method

This method will construct the toast notification with a title and a message. The method will then get the channel URI for all devices that have subscribed to the given category. For each device in that list, the constructed toast notification will be sent.

public void PushToastNotifications(string title, string message, int categoryId)
{
string toastMessage = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<wp:Notification xmlns:wp=\"WPNotification\">" +
"<wp:Toast>" +
"<wp:Text1>{0}</wp:Text1>" +
"<wp:Text2>{1}</wp:Text2>" +
"</wp:Toast>" +
"</wp:Notification>";

toastMessage = string.Format(toastMessage, title, message);

byte[] messageBytes = Encoding.UTF8.GetBytes(toastMessage);

//Send toast notification to all devices that subscribe to the given category
PushToastNotificationToSubscribers(messageBytes, categoryId);
}

private void PushToastNotificationToSubscribers(byte[] data, int categoryId)
{
Dictionary<Guid, Uri> categorySubscribers = GetSubscribersBasedOnCategory(categoryId);

foreach (Uri categorySubscriberUri in categorySubscribers.Values)
{
//Add headers to HTTP Post message.
var myRequest = (HttpWebRequest) WebRequest.Create(categorySubscriberUri); // Push Client's channelURI
myRequest.Method = WebRequestMethods.Http.Post;
myRequest.ContentType = "text/xml";
myRequest.ContentLength = data.Length;
myRequest.Headers.Add("X-MessageID", Guid.NewGuid().ToString()); // gives this message a unique ID

myRequest.Headers["X-WindowsPhone-Target"] = "toast";
// 2 = immediatly push toast
// 12 = wait 450 seconds before push toast
// 22 = wait 900 seconds before push toast
myRequest.Headers.Add("X-NotificationClass", "2");

//Merge headers with payload.
using (Stream requestStream = myRequest.GetRequestStream())
{
requestStream.Write(data, 0, data.Length);
}

//Send notification to this phone!
try
{
var response = (HttpWebResponse)myRequest.GetResponse();
}
catch(WebException ex)
{
//Log or handle exception
}

}
}

private Dictionary<Guid, Uri> GetSubscribersBasedOnCategory(int categoryId)
{
var categorySubscribers = new Dictionary<Guid, Uri>();

using (var context = new NewsReaderEntities())
{
IQueryable<Subscription> selectedSubscribers = from o in context.Subscription
where o.CategoryId == categoryId
select o;
foreach (Subscription selectedSubscriber in selectedSubscribers)
{
categorySubscribers.Add(selectedSubscriber.DeviceId, new Uri(selectedSubscriber.ChannelURI));
}
}
return categorySubscribers;
}

Running the WCF Cloud service

The WCF Cloud service is now completed and you can run it locally or deploy it to Windows Azure.

Deploying the WCF service to Windows Azure

Create a service package

Go to your service project in Visual Studio 2010. Right click the project in the solution explorer and click “Publish”, select “Create service package only” and click “OK”. A file explorer window will pop up with the service package files that got created. Keep this window open, you need to browse to these files later.

Create a new hosted service

Go to https://windows.azure.com and login with the account you used when signing up for the SQL Azure. Click “New Hosted Service” and a pop up window will be displayed. Select the subscription you created for the SQL Azure database, enter a name for the service and a url prefix. Select a region, deploy to Stage and give it a description. Now you need to browse to the two files that you created when you published the service in the step above and click “OK”.  Your service will be validated and created (Note that this step might take a while). In the Management Portal for Windows Azure you will see the service you deployed in Hosted Services once it is validated and created.

Test the deployed WCF service with WCF Test Client

Open the WCF Test Client, on my computer it’s located at: C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\WcfTestClient.exe

Go to the Management Portal for Windows Azure and look at your hosted service. Select the NewsReaderService (type Deployment) and click the DNS name link on the right side. A browser window will open and most likely display a 403 Forbidden error message. That’s OK, we are only going to copy the URL. Then select File and Add Service in WCF Test Client. Paste in the address you copied from the browser and add /NewsReaderService.svc to the end, click OK. When the service is added you can see the methods on the left side, double click AddCategory to add some categories to your database. In the request window enter a category name as value and click the Invoke button.

You can now log in to your SQL Azure database using Microsoft SQL Server Management Studio and run a select on the Category table. You should now see the Categories you added from the WCF Test Client.

Part 2

In part 2 of this article series I will create a Windows Phone 7 application that consumes the WCF service you just created. The application will also receive toast notifications for subscribed categories. Continue to part 2.

You can read my blog at www.breathingtech.com and follow me on twitter @PerOla