每周泰式食谱! 适用于 Windows Phone






4.92/5 (37投票s)
一个完整的 WP7 应用程序,带有一个中央管理网站(ASP.NET MVC4),用于食谱管理和推送通知
目录
引言
本文介绍了 Weekly Thai Recipe 的实现和开发过程。Weekly Thai Recipe 是我开发的第三个 Windows Phone 应用程序。本文也是关于 Windows Phone 开发的第三篇文章。有关我开发 Windows Phone 应用程序的原因的更多信息,可以在 此处 和 此处 找到。
我从我的 第一篇文章 和 第二篇文章 中得到了很多积极的反馈,这促使我决定写一篇新文章,并开放此应用程序的源代码。我希望这篇文章能激励其他人开始开发新的 Windows Phone 应用程序。我在应用程序实现期间撰写了本文,并在 Weekly Thai Recipe 获得认证后不久完成。Weekly Thai Recipe 目前可在 应用商店 中找到。
每周泰式食谱
Weekly Thai Recipe 应用程序提供来自泰国的原创食谱。泰式料理主要由准备得当、香气浓郁的菜肴组成。它以其辛辣而闻名,并始终致力于在每道菜中平衡酸、甜、苦、咸这四种基本味道。
启动应用程序时,会显示三个食谱。每周都会有一个新的原创泰式食谱添加到应用程序的食谱列表中。新食谱通过网络服务接收并存储在手机本地。实时磁贴和 Toast 通知用于通知用户新食谱已可用。
整体架构
Weekly Thai Recipe 的架构由三个主要部分组成。首先是显示食谱的 Windows Phone 应用程序;其次是管理食谱的网络服务和网站;最后是 Microsoft 推送通知服务,它对于能够向 Windows Phone 发送通知和磁贴更新至关重要。

要使用 MPNS 注册应用程序并发送推送通知,您的应用程序必须遵循以下流程。
- 注册 MPNS
- 注册网络服务
- 发送推送通知
- 将推送通知发送到手机
应用程序将自身注册到 MPNS,并检索一个 URI,该 URI 唯一标识了 **此单个** Windows Phone 设备可以接收通知的频道。
应用程序将此 URI 和唯一标识符发送到自定义网络服务(我的实现使用 ASP.NET Web API),该服务将此唯一标识符和 URI 存储在 SQL 数据库中。
通过网站可以向所有已注册的手机发送推送通知。Web 应用程序检索所有已注册的 URI,并将特殊格式的 XML 消息发送到 URI 以通知每部手机。
MPNS 作为中间人接收 XML 消息,然后将其发送到手机。
通过充当中介,MPNS 能够控制谁以及多少消息被发送到手机。例如,它可以防止应用程序向 Windows Phone 发送过多推送通知。
每周泰式食谱手机架构
接收和显示食谱的 Windows Phone 上的应用程序由三个部分组成。

首先是视图,它由 Windows Phone 全景控件组成。应用程序使用 MVVM 模式将视图与逻辑分离。为了协助使用 MVVM 模式,使用了 MVVM Light 框架。
其次是一组服务,允许 ViewModel 或 Model 从本地存储或外部源检索数据。为了协助从食谱网络服务等外部源检索数据,使用了 RestSharp 库用于 Windows Phone。
最后是本地食谱存储层,负责检索和存储本地食谱数据。食谱以 XML 格式存储在手机的隔离存储中。
两种不同场景
这些层支持应用程序支持的两种不同类型的场景(连接和断开连接)。
1. 断开连接场景
当手机(暂时)没有网络连接时,应用程序将无法从外部食谱网络服务接收任何食谱。因此,应用程序具有一套随应用程序分发的 3 个食谱。当用户没有网络连接时,应用程序仍然能够显示三个固定食谱的列表。
应用程序会检查是否有可用的网络连接,如果没有,则从本地 XML 文件接收食谱。应用程序是否有网络连接与否由服务层抽象。视图只需调用 GetRecipes 并接收它们。
2. 连接场景
如果手机有网络连接,ViewModel 会指示服务检索食谱。服务层检测到有网络连接,并向食谱网络服务发送请求,以检查是否已检索到最新食谱。如果没有,它会检索最新食谱,将其存储在手机的隔离存储中,然后将三个固定食谱和动态食谱返回给视图。
通过这样设计应用程序,它不必每次都从网络服务检索食谱。相反,如果它拥有最新食谱,它只会从本地存储返回食谱。在开发完应用程序后,我还注意到有其他框架提供了这类(缓存)服务。例如,AgFx 提供了此功能。我将在应用程序的下一个版本中考虑集成此框架。
视图(全景控件)
全景控件是一个提供视差滚动效果的控件,可用于提供数字杂志的外观和感觉。通常,该控件具有实心背景。但我的应用程序想做一些不同的事情。我发现 Jeff Wilcox 的一篇文章 描述了一种平滑淡化全景控件背景的方法。我决定使用这种方法,并使用一个计时器来轮换全景控件的背景图像。
此视频 展示了应用程序中的效果,每 10 秒显示一个不同的背景,并使用淡入淡出效果。我使用了三张不同的食谱图片。
BackgroundImageRotator
类负责为背景图像画刷提供新图像。
public ImageBrush Rotate()
{
if (CurrentTheme == AppTheme.Dark)
{
string backgroundImageLocation = string.Format("/Images/Panorama{0}.jpg", this.currentBackgroundIndex + 1);
var backgroundImageBrush = new ImageBrush{ ImageSource = new BitmapImage(new Uri(backgroundImageLocation, UriKind.Relative)) };
this.currentBackgroundIndex++;
if (this.currentBackgroundIndex >= NumberOfBackgroundImages)
{
this.currentBackgroundIndex = 0;
}
return backgroundImageBrush;
}
return null;
}
每次调用 Rotate
方法时,它都会遍历可用图像,并返回下一个图像画刷。
安全
网络服务使用表单身份验证来验证请求是否来自受信任的客户端。这种温和的安全形式可防止匿名使用食谱网络服务。 RestSharp 库提供了一个方便简洁的方法来从客户端使用表单身份验证。
使用 RestRequest
来执行对网络服务的请求。ASP.NET MVC4 用于实现食谱网络服务和网站。开箱即用的 ASP.NET MVC4 在帐户控制器上提供了 JsonLogin 操作来执行身份验证。
private static RestRequest CreateAuthenticationRequest()
{
var request = new RestRequest("account/JsonLogin", Method.POST);
request.AddParameter("UserName", Constants.Settings.UserName);
request.AddParameter("Password", Constants.Settings.Password);
return request;
}
如果身份验证成功,ASP.NET 表单身份验证会提供一个授权 cookie,该 cookie 应添加到每个后续请求中。 RestSharp 库提供了一种简单的方法来处理此问题,方法是使用 cookiecontainer
。当向 RestClient
提供 cookie 容器实例时,它会处理将授权 cookie 添加到每个后续请求中。
var client = new RestClient(Constants.Settings.Recipe_Service_Api_Url);
client.CookieContainer = new CookieContainer();
广告
将广告集成到 Windows Phone 应用程序中非常简单。您可以使用不同的广告提供商。以下是您可以在 Windows Phone 中使用的潜在广告提供商列表。
这并非完整列表。Windows Phone 上的广告仍在发展中,新的提供商会出现,现有的提供商也会消失。
我在 Weekly Thai Recipe 中使用了 Microsoft Pubcenter 来添加广告。我选择这个提供商主要是因为集成非常简单。我在食谱详细信息屏幕中显示广告,请参见下方截图的底部。
要使用 Microsoft Pubcenter,您必须在 网站 注册并将在应用程序的视图上放置 adcontrol。您需要在控件中输入您的应用程序 ID 和广告单元 ID。基本上就是这样,您可以更改广告的大小,或者在不同的视图上提供不同大小的控件。
<Grid x:Name="AdvertisementRow" Grid.Row="2"> <my:AdControl AdUnitId="your ad unit id" ApplicationId="your application id" Height="80" HorizontalAlignment="Left" Margin="0,-6,0,0" Name="adControl1" VerticalAlignment="Top" Width="480" /> </Grid>
每周泰式食谱管理架构
如前一节所述,应用程序的食谱和通知管理部分是使用 ASP.NET MVC4 (Beta) 实现的。ASP.NET MVC4 和 WebApi 都用于为 Windows Phone 应用程序提供服务。
中央应用程序提供了两个功能领域:存储和检索食谱的服务,以及通知 Windows Phone 应用程序用户新食谱可用性的能力。

Razor 视图用于提供存储新食谱的功能,它们还提供向连接的手机发送通知的功能。 Dapper 用于从数据库检索和存储数据。Dapper 是一个简单快速的 .Net 对象映射器。Dapper 是一个开源框架,并且被著名的 StackOverflow 网站使用。
推送通知
推送通知(例如 Toast 通知和磁贴更新)是通过将 XML 消息发送到从 Windows Phone 应用程序收到的 URL 来创建的。中央应用程序将所有这些 URL 存储在 SQL 数据库中,以便以后可以使用它们来发送通知。
Toast 通知
ToastSender
类负责向所有连接的手机发送 Toast 通知。当调用 Send
方法时,Toast 通知 XML 消息将被发送到所有连接的手机。
public void Send( string title, string message, PhoneUriCollection phoneUriCollection) { var toastMessage = "" + "<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); var messageBytes = System.Text.Encoding.UTF8. GetBytes(toastMessa ge); foreach (var uri in phoneUriCollection.Values) { try { messageSender.Send(uri, messageBytes, NotificationType.Toast); } catch (Exception error) { ErrorSignal.FromCurrentContext().Raise(error); } } }
Toast 通知 XML 消息包含两个文本字符串,一个表示标题,另一个表示通知的实际文本。此功能通过下面的 Razor 视图提供。
磁贴通知
TileSender
类负责向手机发送磁贴通知。它的工作方式与 NotificationSender 相同,但会向手机发送不同的 XML 消息。
public void Send( string frontTitle, int count, string frontImageLocation, string backTitle, string backImageLocation, string backContent, PhoneUriCollection phoneUriCollection) { var tileMessage = "" + "<wp:Notification xmlns:wp=\"WPNotification\">" + "<wp:Tile>" + "<wp:BackgroundImage>{2}</wp:BackgroundImage>" + "<wp:Count>{1}</wp:Count>" + "<wp:Title>{0}</wp:Title>" + "<wp:BackBackgroundImage>{4}</wp:BackBackgroundImage>" + "<wp:BackContent>{5}</wp:BackContent>" + "<wp:BackTitle>{3}</wp:BackTitle>" + "</wp:Tile> " + "</wp:Notification>"; tileMessage = string.Format( tileMessage, frontTitle, count, frontImageLocation, backTitle, backImageLocation, backContent); var messageBytes = System.Text.Encoding.UTF8.GetBytes(tileMessage); foreach (var uri in phoneUriCollection.Values) { try { messageSender.Send(uri, messageBytes, NotificationType.Tile); } catch (Exception error) { ErrorSignal.FromCurrentContext().Raise(error); } } }
发送推送通知 XML 消息
MessageSender
类负责将 XML 消息的字节发送到服务器。将 "X-WindowsPhone-Target" 和 "X-NotificationClass" 标头添加到 Http 请求中,以指示通知的类型以及何时将通知发送到手机。
public void Send(Uri uri, byte[] message, NotificationType notificationType) { var request = (HttpWebRequest)WebRequest.Create(uri); request.Method = WebRequestMethods.Http.Post; request.ContentType = "text/xml"; request.ContentLength = message.Length; request.Headers.Add( "X-MessageID", Guid.NewGuid().ToString()); switch (notificationType) { case NotificationType.Toast: request.Headers["X-WindowsPhone-Target"] = "toast"; request.Headers.Add( "X-NotificationClass", ((int)BatchingInterval.ToastImmediately) .ToString(CultureInfo.InvariantCulture)); break; case NotificationType.Tile: request.Headers["X-WindowsPhone-Target"] = "token"; request.Headers.Add( "X-NotificationClass", (int)BatchingInterval.TileImmediately). ToString(CultureInfo.InvariantCulture)); break; default: request.Headers.Add( "X-NotificationClass", (int)BatchingInterval.RawImmediately). ToString(CultureInfo.InvariantCulture)); break; } using (var requestStream = request.GetRequestStream()) { requestStream.Write(message, 0, message.Length); } try { var response = (HttpWebResponse)request.GetResponse(); } catch (WebException ex) { Debug.WriteLine(string.Format("ERROR: {0}", ex.Message)); throw ex; } }
Weekly Thai Recipe 使用两种类型的推送通知:Toast 和 Tile。第三种类型,即原始通知,未使用。原始通知可用于将自定义数据发送到您的 Windows Phone 应用程序。请注意,如果您的应用程序未运行,原始通知将被丢弃。
视图与服务之间的通信
视图与服务之间的通信使用 jQuery 实现。服务使用 WEB API 控制器实现。
[Authorize] public class ToastController : ApiController { private readonly ISubscriptionRepository subscriptionRepository; private readonly ToastSender toastSender; public ToastController(ISubscriptionRepository subscriptionRepository, ToastSender toastSender) { this.subscriptionRepository = subscriptionRepository; this.toastSender = toastSender; } [HttpPost] public void Send(string toastTitle, string toastMessage) { try { PhoneUriCollection phonesCollection = subscriptionRepository.GetAll(); toastSender.Send(toastTitle, toastMessage, phonesCollection); } catch (Exception error) { ErrorSignal.FromCurrentContext().Raise(error); throw new HttpResponseException(error.Message, HttpStatusCode.InternalServerError); } } }
ToastController 负责接收 Toast 发送请求。它包含一个标题和一个应发送到所有已注册手机的消息。
一个 JavaScript 类用于实际将消息发送到控制器。initialize 方法将一个方法绑定到 sendToastButton 的 click 事件。该方法检索标题和消息文本字段的值,并使用 $.ajax jQuery 方法将其发送到控制器。方法成功时,会显示一个提示,表明 Toast 已发送。
var toastInitializer = function () {
var initialize = function (sendToastUrl) {
$('#sendToastButton').click(function () {
var toastTitle = $('#toastTitle').val();
var toastMessage = $('#toastMessage').val();
if (toastTitle.length > 0 && toastMessage.length > 0) {
$.ajax({
url: sendToastUrl,
success: function (data) {
alert('Toast is send');
},
data: {
toastTitle: toastTitle,
toastMessage: toastMessage
},
dataType: "json",
type: "POST"
});
}
});
};
return {
initialize: initialize
};
};
客户端的错误处理通过覆盖 jQuery $.ajax error 方法进行集中管理。
var generalErrorHandler = function() { var initialize = function() { $.ajaxSetup({ "error": function (xhr) { var errorMessage = $('#errorMessage'); if (errorMessage.length > 0) { errorMessage.html(xhr.responseText); } var errorDialog = $('#errorDialog'); if (errorDialog.length > 0) { errorDialog.show(); } } }); }; return { initialize: initialize }; };
在共享的 ASP.NET MVC _Layout.cshtml 文件中添加了一个错误 div,用于显示实际的错误。
<body>
<div id="errorDialog" class="errorDialog" style="display: none">
<div id="errorIcon" class="errorIcon" style="cursor: pointer;" ><!-- --></div>
<div class="messageText">
<span id="errorMessage"></span>
</div>
</div>
@this.RenderBody()
</body>
通用的错误处理程序和所有必需的 JavaScript 类都在 main 视图的 $(document).ready 方法中初始化。
$(document).ready(function () { var errorHandler = new generalErrorHandler(); errorHandler.initialize(); var toast = new toastInitializer(); toast.initialize('@Url.Action("Send", "api/Toast")'); var tile = new tileInitializer(); tile.initialize('@Url.Action("Send", "api/Tile")'); var recipe = new recipeSaver(); recipe.initalize('@Url.Action("Save", "api/Recipe")'); });
Dapper 数据访问
我使用 Dapper 进行数据访问,因为使用像 Entity Framework 或 NHibernate 这样的完整 ORM 来管理两三个数据库表来说太过了。Dapper 被称为微型 ORM。它提供了完整 ORM 所提供服务的一个子集。这使您能够更强大地控制记录如何在数据库中存储和检索。
Dapper 提供了参数化查询和实现这些查询结果的功能。例如,以下源代码用于从数据库检索所有已注册的手机(订阅)。
IEnumerable<subscription> subscriptions = connection.Query<subscription>(@"select PhoneId, Uri from subscription"); </subscription></subscription>
Dapper 负责执行查询并创建一个由数据库数据填充的订阅实例列表。表列和类属性之间的映射基于列名和属性名。Dapper 识别这两个名称应该相同。
工具和框架
下面是开发 Weekly Thai Recipe 所使用的工具和框架列表。
Windows Phone 应用程序
中央管理应用程序
使用源代码
源代码包中包含两个解决方案。要打开 Windows Phone 解决方案,您需要安装 Visual Studio 2010 和 Windows Phone SDK。要打开中央食谱管理解决方案,您需要安装 ASP.NET MVC4 (beta)。
源代码包中有一个独立的 SQL 服务器数据库,您可以将其附加到本地 SQL 服务器以创建一个功能齐全的应用程序。如果您使用此数据库,可以使用用户名 Codeproject 和密码 Codeproject 登录。
结论
该应用程序可在 应用商店 中找到,完整源代码可从文章顶部下载。如果您喜欢这篇文章,请投票或评论。谢谢。
接下来呢?
我的下一个 Windows Phone 项目将是使用 XNA 框架的游戏。我仍在确定细节,但它将以文章的形式出现在 CodeProject 上。
历史
- v1.0 初始版本
- v1.1 更新了中央应用程序的源代码,以使用 ASP.NET MVC4 的 RC 版本(感谢 Jeff Albrecht 的提示!)
- v1.2 在源代码 zip 中添加了一个独立的数据库,以创建一个完整的包。