构建一个端到端的Windows商店应用程序-第2部分: 集成云服务

[原文发表地址]  Building an End-to-End Windows Store App - Part 2: Integrating Cloud Services

[原文发表时间] 2012-8-28 9:12

我之前的博客中,我已经开始使用Visual Studio express 2012 for Windows 8 构建一个Windows 商店应用程序, 且在这个过程中结合一些Windows特性,比如动态磁贴和搜索。然而, 这些天来很多应用程序都得益于使用和集成各种在云中运行的后台服务,我希望我的应用程序也能获益。

在这方面, 我们收到的开发者的反馈中涉及到最多的一个领域是希望采用turn-key服务集,以便于他们的设备应用程序使用,而不需要生成,部署和管理他们自己的服务。为了解决这方面的需要,今天我们发布了新Windows Azure移动服务预览版。这个版本包括针对Windows 商店应用程序的客户端SDK, 这些应用程序可以是使用JavaScript, C#, Visual Basic 或者C++编写的, 且提供存储, 消息推送等等功能(包括可以编写JavaScript脚本在服务器上运行)。

在本篇博客中,我将使用各种形式的后台服务来扩展我的应用程序,包括利用Windows Azure 移动服务。

启用共享

除了动态磁贴和搜索之外,另一个我想在我的应用程序中启用的特性是共享。就像在https://msdn.microsoft.com/library/windows/apps/hh465261.aspx中描述的一样, Windows 8使应用程序可以通过Share charm发布数据给其它应用程序使用。例如, 我想通过邮件给朋友发送一封有趣的RSS文件,我不需要在我的应用程序中创建所有的UI和围绕邮件的逻辑。取而代之的是我只需要分享出相关的内容,并且我系统中的一个邮件客户端(比如Mail应用程序)可以通过注册为共享目标来处理相关方面的操作。通过这种方式,我可以通过其它应用程序将服务集成到我的应用程序中。

为了实现此功能,我在我的ItemDetailPage.xaml.cs文件中添加了几行代码:

 private DataTransferManager _dtm;
  
 protected override void OnNavigatedTo(NavigationEventArgs e)
 {
     base.OnNavigatedTo(e);
     _dtm = DataTransferManager.GetForCurrentView();
     _dtm.DataRequested += ShareDataRequested;
 }
  
 protected override void OnNavigatedFrom(NavigationEventArgs e)
 {
     base.OnNavigatedFrom(e);
     _dtm.DataRequested -= ShareDataRequested;
     _dtm = null;
 }
  
 void ShareDataRequested(DataTransferManager sender, DataRequestedEventArgs args)
 {
     var toShare = (SampleDataItem)flipView.SelectedItem;
     if (toShare != null)
     {
         args.Request.Data.Properties.Title = toShare.Title;
         args.Request.Data.Properties.Description = string.Empty;
         args.Request.Data.SetHtmlFormat(HtmlFormatHelper.CreateHtmlFormat(toShare.Content));
     }
 }

以上代码在当前DataTranferManager中为DataRequested事件注册了一个委托。当请求到来的时候,它会为分享到DataRequestedEventArgs 结构的数据填写一些参数,然后自己保存数据。在这个例子中,被保存的数据是HTML标记,所以我首先使用来自WinRT的HtmlFormatHelper.CreateHtmlFormat方法来确保所有标头和标尾在内容上是正确的,然后使用SetHtmlFormat方法将数据发送到目的地。剩下的事情Windows会处理。

你可以在这里看到结果。在项页面,我已使用Share charm来分享我的一篇博文,选择Mail应用程序为目标地址:

启用漫游

我不想强迫应用程序的用户在每次运行应用程序时都要重新输入他们最喜爱的博客 feeds(源)。而是希望能够在每次的运行中保存数据,就像之前运行载入的feed链接能够在下次运行时自动载入。此外,如果一个用户在多个Windows 设备上安装了我的应用程序,我不希望他们要在每个设备上重新输入相同的feeds。而是希望他们最喜欢的这些feeds能够跟着用户漫游,无论他们登录哪个设备都将有效。

在过去,这样一个特性需要执行一个定制的后台服务--用来与应用程序沟通的服务。然而,在Windows 8和WinRT中,我们把这些服务集成到了内置中。就像在博客 https://blogs.msdn.com/b/windowsappdev/archive/2012/07/17/roaming-your-app-data.aspx中描述的那样,使用ApplicationData类和从ApplicationData.Current.RoamingSetting.Values中返回的集合,应用程序数据可以被漫游。简单地存储一对key/value到值中,无论在哪里打开你的应用程序,它都将可用。

为了利用这个优势,我在应用程序中添加了两个方法(在新PersistedState.cs文件中),一个用来序列化我收集的feeds到RoamingSettings中,另一个用来反序列化并且从RoamingSettings中返回数据:

 internal static void SaveFeedUrls()
 {
     var serializer = new DataContractSerializer(typeof(List<string>));
     var feeds = SampleDataSource.AllGroups.Select(g => g.UniqueId).ToList();
     using(var tmpStream = new MemoryStream())
     {
         serializer.WriteObject(tmpStream, feeds);
         ApplicationData.Current.RoamingSettings.Values["feeds"] = tmpStream.ToArray();
     }
 }
  
 internal static IEnumerable<string> LoadFeedUrls()
 {
     if (!ApplicationData.Current.RoamingSettings.Values.ContainsKey("feeds"))
         return Enumerable.Empty<string>();
  
     var serializer = new DataContractSerializer(typeof(List<string>));
     using(var tmpStream = new MemoryStream(
         (byte[])ApplicationData.Current.RoamingSettings.Values["feeds"]))
     {
         return (List<string>)serializer.ReadObject(tmpStream);
     }
 }

然后,在GroupedItemPage.LoadState(在GroupdItemPage.xaml.cs中)中,我添加所有之前保存的feeds:

 protected override async void LoadState(
     object navigationParameter, Dictionary<string, object> pageState)
 {
     this.DefaultViewModel["Groups"] = SampleDataSource.AllGroups;
     foreach(var feed in PersistedState.LoadFeedUrls())
         await AddFeedAsync(feed);
 }

在成功地加添一个新的feed后,我会添加一个调用来保存现在的设置:

 if (await SampleDataSource.AddGroupForFeedAsync(feed))
 {
     UpdateTile();
     PersistedState.SaveFeedUrls();
 }

通过这个方法,feeds 就成功地被保存下来,用户可以在他们的Windows设备间使用我的应用程序加载这些feeds 。

启用定制后台服务

在启用共享中,我利用了系统中被其它应用程序所支持的服务优势。在启用漫游中,我利用了被WinRT所支持的本机服务来使其在指定用户设备间共享数据的优势。不过在用户间共享数据我又能利用什么优势呢?例如, 我想让一个服务允许用户发布feeds给其他用户, 让他们可以找到并且加载到应用程序中。

为了达到这一点,我可以利用新的Windows Azure 移动服务,它使从云中存放和获得数据变成了一桩小事(一个简短的学习指南可以从这里获得)。

首先,我通过Windows Azure门户网站创建一个新的移动服务:

在几秒钟之内,我就有了一个准备就绪的新移动服务等待我去定制:

通过门户网站界面,我在我的移动服务中创建一个新的”表”用来存储有关feeds的信息,这些信息被我的应用程序所共享:

我不需要为表配置任何架构,当我把数据加入的时候,服务默认会为我自动处理好这些数据。然而, 有一件事我希望去做的是我可以利用移动服务功能的优势, 编写服务器端JavaScript脚本来处理插入, 更新,读写和删除事件。对我而言,在同样的feed URL提交不止一次的时候,我不需要重复存储条目, 所以我写了一个基本的” Insert(插入)”脚本帮助我过滤掉这些重复的条目:

 function insert(item, user, request) {
     tables.getTable("SharedFeed").where( { Url : item.Url }).read({
               success : function(results) {
                   if (results.length > 0) {
                       request.respond(200, results[0]);
                   }
                   else {
                       request.execute();
                   }
               },
               error: function(err) {
                   console.error(err); 
                   request.respond(500, "Error"); 
               }
           });
 }

这个编写可以使用Azure门户网站的浏览器编辑器来完成:

当有新的条目要插入表的时候,这个”插入”方法就会被自动调用。我的脚本就会搜索SharedFeed表以查找与要插入feed匹配的URL。如果该条目已经存在,脚本就会发送一个答复给调用者,把操作当作nop指令处理。然而如果这个URL还不存在, 脚本就会开始去执行最初的请求, 然后添加一条记录。 当然, 如果存在错误, 我希望能够诊断出错误, 所以我记录它并且发送给客户端一个通用的”错误”消息, 这样我也不会泄露任何敏感信息。所有的日志信息都可以在我的服务的日志(log)板块中找到:

就这样,我的服务完全配置好了,现在我需要把它使用到我的应用程序中。这么做最简单的方法是通过Windows Azure移动服务SDK. 安装SDK后,我会使用Visual Studio中的添加引用…… 对话框来为服务添加客户端库。该库是通过Windows\扩展类作为一个扩展SDK可用的:

现在我可以使用客户端API与我的移动服务互动。首先, 任何用户加载feed的时候, 我希望将它存储到我之前配置好的”SharedFeed”表中。为了实现这个,我为SharedFeed类型定义了三个参数: Id, Title(标题)和Url(Id将是主键):

 [DataTable(Name="SharedFeed")]
 public class SharedFeed
 {
     [DataMember(Name="Id")]    public int Id { get; set; }
     [DataMember(Name="Title")] public string Title { get; set; }
     [DataMember(Name="Url")]   public string Url { get; set; }
 }

在GroupedItemPage.xaml.cs中,我写了以下方法来从我的应用程序将一条记录插入到服务中:

 async void ShareFeedOnServerAsync(string title, string url)
 {
     try
     {
         var table = App.MobileService.GetTable<SharedFeed>();
         await table.InsertAsync(new SharedFeed { Title = title, Url = url });
     }
     catch (Exception exc) { Debug.WriteLine(exc.Message); }
 }

并且我修改AddFeedAsync方法, 这个时候除了更新动态磁贴和存储数据外,还将激活ShareFeedOnServer:

 if (await SampleDataSource.AddGroupForFeedAsync(feed))
 {
     UpdateTile();
     PersistedState.SaveFeedUrls();
     ShareFeedOnServerAsync();
 }

现在每添加一个feed, 都会自动保存到我的服务中。你可以在Azure门户网站这里看到我加载一些feeds到我的应用程序之后的结果:

现在,我需要将这些记录展示给用户,让他们可以从feeds的列表中选择。为了实现这个,我再次使用了Visual Studio中的添加项…… 对话框,正如我之前为搜索所做的一样,但这次选择添加新的Split Page(分页)屏幕到我的应用程序中:

这个新的PopularFeeds.xaml文件包含比我实际需要的更多XAML。我没有显示每个元素的任何“细节”,我可以删除所有跟细节有关的界面:

  • 第二列的定义
  • 与显示每个项目详细信息有关的整个ScrollViewer
  • 与任何详细控件有关的所有动画关键桢,例如itemDetail,itemDetailTitlePanel和itemDetailGrid

为了显示列表结果,我将一些新的的XAML添加到这个文件的Page.Resources中。, 基于StandardStyles.xaml 文件中的Standard130ItemTemplate,我更新了PopularFeeds.xaml中的所有代码, 用Standard130ItemTemplate或者Standard80ItemTemplate来替换对新FeedResultItemTemplate的引用:

 <DataTemplate x:Key="FeedResultsItemTemplate">
     <Grid Margin="6">
         <Grid.ColumnDefinitions>
             <ColumnDefinition Width="*"/>
         </Grid.ColumnDefinitions>
         <StackPanel Grid.Column="0" Margin="10,0,0,0">
             <TextBlock Text="{Binding Title}" 
                        Style="{StaticResource TitleTextStyle}" 
                        TextWrapping="NoWrap"/>
             <TextBlock Text="{Binding Url}" 
                        Style="{StaticResource CaptionTextStyle}" 
                        TextWrapping="NoWrap"/>
         </StackPanel>
     </Grid>
 </DataTemplate>

我还用一些新的属性来修改了现有的resultListView,这样用户就可以在列表中点击(不能长时间选择)一个元素:

 IsItemClickEnabled="true"
 ItemClick="ItemListView_Click”
 SelectionMode="None"

为了确保在添加了可点击的feed后,点击一个元素时能够导航回之前的屏幕,我在PopularFeeds.xaml文件中添加了如下代码:

 async void ItemListView_Click(object sender, ItemClickEventArgs e)
 {
     var url = ((SharedFeed)e.ClickedItem).Url;
     await SampleDataSource.AddGroupForFeedAsync(url);
     PersistedState.SaveFeedUrls();
     this.IsEnabled = false;
     this.Frame.GoBack();
 }

在新页面的后台代码中,除了DetermineVisualState方法之外,我删除了所有方法(我删除了其中的logicalPageBack概念),并且重新了它的LoadState方法,用我移动服务中ShareFeed表的所有条目来填充列表结果,但是只显示那些我之前没有加载过的条目:

 protected override async void LoadState(
     object navigationParameter, Dictionary<string, object> pageState)
 {
     var items = 
         (await App.MobileService.GetTable<SharedFeed>().ToEnumerableAsync())
         .Where(item => SampleDataSource.GetGroup(item.Url) == null)
         .ToList();
     this.DefaultViewModel["Items"] = items;
 }

就这样,我的 Popular Feeds页面就做好了,它会引导你到结果屏幕,用户可以在上面轻松地选择我应用程序中其他用户使用的feeds。

在我应用程序中的主页面中(GroupedItemPage.xaml文件),我需要创建一个简单的方法让用户可以导航到Popular feeds的页面。因此,我在之前创建的AppBar中添加了一个新的按钮(与我之前加入AppBar的按钮相比,我在StandardStyle.xaml中取消注释了必要的按钮样式):

 <Button x:Name="btnPopularFeeds" Click="btnPopularFeeds_Click"
     Style="{StaticResource DownloadAppBarButtonStyle}" />

我添加按钮的点击事件(在GroupdItemPage.xaml.cs中)来定位到Popular feeds页面:

 private void btnPopularFeeds_Click(object sender, RoutedEventArgs e)
 {
     this.Frame.Navigate(typeof(PopularFeeds));
 }

启用消息推送

我的“popular feeds”特性已经基本完成。用户现在可以在我应用程序中与其他用户共享他们的feeds,并且可以轻松地添加其他用户使用的feeds。然而,一个用户添加了新的feeds,而并没有通知其他用户。为了解决这个问题,我想让Visual Studio, Windows 8和Windows Azure 移动服务一起支持消息推送。我只要稍作努力就可以添加消息推送功能到我的应用程序中。

首先,我需要配置我的移动服务使它支持消息推送。对于这一点,我打开我的浏览器并且导航到https://manage.dev.live.com/build,按照里面列出的步骤来注册我的应用程序。我必须填写一个简短的表格,用的是来自我应用程序中的Package.appxmanifest的Packaging部分的两条信息:

当我点击“I accept(我接受)”按钮时,在我面前呈现了三个信息:一个新软件包名称,一个客户端密码和一个软件包安全标识符。我将软件包名称复制到我应用程序Package.appxmanifest中的“软件包名称”字段中(在清单中,我还要将“Toast capable”字段改为“Yes(是)”),并且我将所有客户端密码和软件包标识符复制到我移动服务的“push(推送)”选项卡:

随着服务配置方式的出台,我需要增强我的服务以使它可以存储连接客户端的信息,这样才能给他们推送消息。因此,我创建了一个新“Channel”表,按照我创建“SharedFeed”表相同的步骤来做。我还要复制相同的“插入”脚本以淘汰重复内容,代码唯一要修改的就是查询的表格不同。

就这样,我设置了客户端跟踪,并且当有新的feed加入的时候,我能够得给他们发送所有的消息。为了达到这一目的,我再次为我的ShareFeed表修改了“插入”脚本,添加了一个“sendNotification”方法,并且在新项目插入后从“插入”脚本中调用它。

 function sendNotifications(item) {
     tables.getTable("Channel").read({
         success: function(results)
         {
             results.forEach(function(channel) {
                 push.wns.sendToastText04(channel.channelUri, {
                     text1: "New feed recommended!",
                     text2: item.Title,
                     text3: item.Url
                 }, {
                     success: function(data) { console.log(data); }
                 });
             });
         }
     });
 }
  
 function insert(item, user, request) {
     ...
                 request.execute();
                 sendNotifications(item);
     ...
 }

当“insert(插入)”方法调用“sendNotification”时,它将从channels表中获得所有的注册客户端,并且给每个客户端发送包含新加feed消息的通知。

这是服务端所有要做的事情。在我的客户端,我也需要为通知而注册,把Windows为我创建的Uri推到服务. 在我的App.xaml.cs文件中,我添加了如下的方法,在所有其它初始化完成以后从结构函数中调用它:

 private async void RegisterForPushNotificationsAsync()
 {
     try
     {
         var channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();
  
         var jo = new JsonObject();
         jo.Add("channelUri", JsonValue.CreateStringValue(channel.Uri));
  
         var table = App.MobileService.GetTable("Channel");
         await table.InsertAsync(jo);
     }
     catch(Exception exc) { Debug.WriteLine(exc.Message); }
 }

这是用WinRT中的PushNotiicationChannelManager为我的应用程序来创建一个”channel”。然后我需要把channel的Uri存储到我之前创建的移动服务表中. 要做到这一点,不是创建一个新类来存储Uri(就像我为SharedFeed做的一样), 而是使用移动服务客户端支持的非类型化方法:创建一个新的JasonObject,把channelUri属性存储到里面,并且把JSON插入到服务中。

就这样,我的推送消息就都完成了。现在,无论何时一个新的项目添加到服务的”SharedFeed”表中时,我服务器端的”插入”方法就会被调用, 从而调用我的”sendNotification”和内建的”sendToastText04”方法,最后就把该Feeds推送到所有客户端:

如果我想,我还可以再进一步修改。比如,在使用应用程序的时候,用户可能会收到他们已经在阅读的feeds的不必要提示。为了解决这一问题,我添加以下代码到我的RegisterForPushNotificaitonAsync方法中:

 channel.PushNotificationReceived += (s, e) =>
 {
     var node = e.ToastNotification
                 .Content
                 .SelectSingleNode(@"//text[@id=""3""]");
     e.Cancel = node != null && 
                SampleDataSource.GetGroup(node.InnerText) != null;
 };

这个事件处理器会在Windows显示toast之前被激活 (不过只局限于前台应用程序)。 代码会检查XML是否引用了一个已经添加的数据源,如果是,它就会取消toast的消息推送. 我当然还可以再进一步,例如让用户把feeds存储到我的应用程序中,并且只推送给之前没有注册过该feed的用户.

也请注意,我没有为我的服务配置任何形式的安全措施。Windows Azure 移动服务对身份和访问控制提供了丰富的支持,但是基于本篇博文的目的,我选择不涉及任何相关内容。

启用广告

现在我的应用程序已经基本完成了,我想再做一件事:启用广告。对于这一点,我将利用Microsoft Ads SDK for Windows 8.

首先,我导航到https://advertising.microsoft.com/windowsadvertising/developer, 按照里面描述的详细步骤,开始安装SDK,这只需要花费几秒钟. 一旦安装完成, 我就导航到https://pubcenter.microsoft.com 注册一个账户,并且为我的应用程序注册:

这不仅需要提供我应用程序的名字和类型, 还要定义一个“广告单元”, 提供我应用程序的广告的大小和分类等信息:

只要完成了这些,我应用程序就已经注册来显示广告了,并且系统会提供给我一个应用程序ID和一个我刚包含在我应用程序中的广告单元ID:

在安装了SDK后,返回到Visual Studio, AdControl现在就准备好可以为我所用了:

我把该控件拖拽到Visual Studio的设计器中,将XAML放在我想要的位置上 ,然后填入注册网页提供的我的应用程序ID和广告单元ID:

现在当我运行我的应用程序时,我就能看到广告成功地被显示了:

总结

在这个由两部分组成的博文中, 我结合Windows 功能和利用各种后台服务详细地创建了一个简单的Windows商店应用程序。 其中实际要我编写的代码很少,许多繁重的处理都由Visual Studio, Windows 8和Windows Azure承担了。一路上,我建立了一个很好的用户体验,其中包括动态磁贴、搜索、共享协议、跨设备的漫游数据和其他用户共享数据、消息推送和广告。

这些博文向你重点展示了这些工具和平台所能提供的一些强大功能, 我将非常高兴能够看到你们用它们来创建程序。

万福!