构建一个端到端Windows商店应用程序 – 第1部分

[原文发表地址]   Building an End-to-End Windows Store App - Part 1

[原文发表时间] 26 Aug 2012 7:04 PM

之前的帖子中,我提到连接设备和持续服务的主题正在成为Visual Studio2012的一个重点关注领域之一。这包括在Visual Studio 2012中创建一流的工具集来设计和构建令人惊叹的Windows应用程序和服务。

随着Windows 8和Visual Studio2012的发布,我决定探索和记录一个基本Windows商店应用程序的端对端开发,它会使用后端服务。针对这项工作,我选择使用Visual Studio Express 2012 for Windows 8。

在这里,我的首要目标是突出现在要建立一个现代的连接体验是多么地简洁易懂。出于对简洁性和最小化代码调用的考虑,我会创建一个简单的RSS阅读器:“News by Soma”。在这个两部分的博客贴子中,我会记录创建这个应用程序的体验。希望我的记录足够详细,使你能够跟着博客的内容同样创建(以及拓展)你自己版本的应用程序。

开始

由于我的目标是展示完整的端对端体验,我从一个没有安装Visual Studio的Windows 8 系统开始。我下载了Visual Studio Express 2012 for Windows 8 安装程序,点击安装向导,然后开始安装:

几分钟之内,我就建立起了我的开发环境,并运行了起来:

从File| New Project,我用C#创建了一个新的Windows Store“Grid App (XAML)”:

这个Grid应用程序的模板很好地描绘了我脑中的情景,那就是能够获得多个RSS源(称为“组”),每个组合中包含多个帖子(称为每组中的“项”)。这个模板提供了所有的基础需求来很快地创建这样一个应用程序。在这个项目被创建之后,我按F5来查看我的(还未修改过)应用程序的操作。

基本结构就位以后,我现在可以开始根据我自己的特殊需求来自定义它了。

配置基本的应用程序属性

我当然想要我的应用程序更美观,所以我在Visual Studio中花了一些时间用Package.appxmanifest文件自定义它的属性。打开这个文件,它呈现了一个详细的Visual Studio编辑器。

在这里我按照我自己的方式配置了我的应用程序的磁贴(Windows 起始页)和初始屏幕(应用程序开始时显示的)。这包括创建一些图片,我用自己的照片来开始,然后在“Paint(画图)”中创建各种图片的剪裁来适应配置编辑器的特定大小(例如150x150的Logo,310x150的Wide Logo,等等)

这造就了一个很好的磁贴体验,当应用程序被固定到我的开始界面时,无论是使用小磁贴:

或者是宽磁贴

我的应用程序现在有一个很好的初始屏幕体验。

获取数据

这个Grid应用程序模板从一个SampleDataSource类型(在DataModel\SampleDataSource.cs文件中)获得所有的数据,这个类型公开了一个AllGroups属性,而此属性返回一个ObservableCollection<SampleDataGroup>。这使得UI能够数据绑定到一个组集合,每个集合都被通用数据模型类型SampleDataGroup所代表。SampleDataGroup又包含SampleDataItem的实例集合。

在我的app中,SampleDataGroup映射到了RSS的源,SampleDataItem映射到了源中的项。出于简洁性的考虑, 我简单地重用了它们,而不是用我自定义的数据类替换SampleDataGroup和SampleDataItem。这个模板包含了这些类足够的相关属性,所以我实际上根本不需要去修改它们 ;取而代之的是,我只需要用正确的数据修改SampleDataSource来填充和返回它们的实例。

在SampleDataSource类型中有相当数量的代码包含在这个模板中,它们中的大部分是关于在之前的截屏中填充无意义的文字项目“lorem ipsum”。我把它们全都删除了,并且用一个简单的静态声明(修复了所有执行过程中的引用)替换了AllGroups属性。

    1: public static readonly ObservableCollection<SampleDataGroup> AllGroups =
    2:     new ObservableCollection<SampleDataGroup>();

我的UI能够继续绑定到AllGroups,它的初始值为空。随着新的组(RSS源)被加入到AllGroups,UI会被自动地提示这项的加入,并且会相应地自我更新。因此,我需要公开一个方法来添加组:

    1: public static async Task<bool> AddGroupForFeedAsync(string feedUrl)
    2: {
    3:     if (SampleDataSource.GetGroup(feedUrl) != null) return false;
    4:  
    5:     var feed = await new SyndicationClient().RetrieveFeedAsync(new Uri(feedUrl));
    6:  
    7:     var feedGroup = new SampleDataGroup(
    8:         uniqueId: feedUrl,
    9:         title: feed.Title != null ? feed.Title.Text : null,
   10:         subtitle: feed.Subtitle != null ? feed.Subtitle.Text : null,
   11:         imagePath: feed.ImageUri != null ? feed.ImageUri.ToString() : null,
   12:         description: null);
   13:  
   14:     foreach (var i in feed.Items)
   15:     {
   16:         string imagePath = GetImageFromPostContents(i);
   17:         if (imagePath != null && feedGroup.Image == null)
   18:             feedGroup.SetImage(imagePath);
   19:         feedGroup.Items.Add(new SampleDataItem(
   20:             uniqueId: i.Id, title: i.Title.Text, subtitle: null, imagePath: imagePath,
   21:             description: null, content: i.Summary.Text, @group: feedGroup));
   22:     }
   23:  
   24:     AllGroups.Add(feedGroup);
   25:     return true;
   26: }

使用来自Windows Runtime(WinRT)中的SyndicationClient 和C#中新的async/await keywords,我可以在请求的URL中异步地下载源。然后我创建了一个SampleDataGroup来代表源,用关于源的信息填充它,这些信息是我传递到SyndicationFeed的。然后,对于每一个syndication feed中的项,我映射它的属性到一个新的SampleDataItem中去。这些项都被加入到组中,然后这些组被加入到AllGroup集合中去。随着这些工作的完成,我基本上教完了这个应用程序如何获取所有它需要的数据。

这里剩下的一段代码是和图片有关的。UI知道如何绑定SampleDataGroup和SampleDataItem,包括向每个组和项展示一张图片。通常情况下,RSS源项是与图像不相关联的,但是我需要一些适当和有趣的东西在UI中为每个源项展示。因此,我还有一个函数,它解析RSS项目,寻找PNG和JPG图像,返回第一个它找到的完全合格的路径。

    1: private static string GetImageFromPostContents(SyndicationItem item)
    2: {
    3:     return Regex.Matches(item.Summary.Text, 
    4:             "href\\s*=\\s*(?:\"(?<1>[^\"]*)\"|(?<1>\\S+))", 
    5:             RegexOptions.None)
    6:         .Cast<Match>()
    7:         .Where(m =>
    8:         {
    9:             Uri url;
   10:             if (Uri.TryCreate(m.Groups[1].Value, UriKind.Absolute, out url))
   11:             {
   12:                 string ext = Path.GetExtension(url.AbsolutePath).ToLower();
   13:                 if (ext == ".png" || ext == ".jpg") return true;
   14:             }
   15:             return false;
   16:         })
   17:         .Select(m => m.Groups[1].Value)
   18:         .FirstOrDefault();
   19: }

最后,在我能够真正运行这个应用程序之前,我还需要做一个更改: 修改GroupedItemsPage.LoadState 方法(在GroupedItemsPage.LoadState.xaml.cs文件中)来使用这个新的SampleDataSource.AddGroupForFeedAsync方法。我在模板中用一行代码将AllGroups和UI联系起来,以此代替了LoadState,并且加入了一些额外的代码来用一些博客初始填充UI。

    1: protected override async void LoadState(
    2:     object navigationParameter, Dictionary<string, object> pageState)
    3: {
    4:     this.DefaultViewModel["Groups"] = SampleDataSource.AllGroups;
    5:  
    6:     // temporary hardcoded feeds
    7:     await SampleDataSource.AddGroupForFeedAsync("https://blogs.msdn.com/b/somasegar/rss.aspx");
    8:     await SampleDataSource.AddGroupForFeedAsync("https://blogs.msdn.com/b/jasonz/rss.aspx");
    9:     await SampleDataSource.AddGroupForFeedAsync("https://blogs.msdn.com/b/visualstudio/rss.aspx");
   10: }

这样就完成了。我现在可以再按F5,就能看到RSS数据填充到我的应用程序中去了。

主分组项页面:

组页面(当我在主页上点击一个组标题):

项页面(当我在主页或者组页面点击一个项目):

这里有一点要注意的是,我还没有为项页面修改默认的模板,它使用一个RichTextBlock来展示帖子的内容。结果是,RSS项中的HTML显示为HTML的源代码,而不是呈现的内容。

为了让这更美观,我更新了模板来展示HTML。ItemDetailPage.xaml用一个FlipView控件来展示SampleDataItem,通过DataTemplate,它为模板项使用一个UserControl。我用如下的XAML(它使用一个WebView控件 )替换了这个UserControl(它在原始代码中包含RichTextBlock相关的控件):

 <UserControl Loaded="StartLayoutUpdates" Unloaded="StopLayoutUpdates">
     <Grid>
         <Grid.RowDefinitions>
             <RowDefinition Height="Auto"/>
             <RowDefinition Height="*"/>
         </Grid.RowDefinitions>
         <TextBlock Margin="10,10,10,10" Text="{Binding Title}"
                    Style="{StaticResource SubheaderTextStyle}" 
                    IsHitTestVisible="false" Grid.Row="0" />
         <WebView local:WebViewExtension.HtmlSource="{Binding Content}" Grid.Row="1"/>
     </Grid>
 </UserControl>

WebView 控件本身没有属性来允许我直接绑定WebView到已有的HTML字符串contentI,不过我从Tim Heuer那得到关于一个

HtmlSource拓展属性的代码,它使用WebView的NavigateToString方法来实现相同的事情。随着这些加入我的项目之后,现在我能看到源项在应用程序中美观地显示了:

用户交互

在之前的获取数据部分中,我简单地对我要显示的源进行了硬编程。然而,我想要允许用户手动地输入他们自己选择的源,所以我会稍微增加模板的UI来启用用户交互。

常见的Windows商店应用程序设计元素之一是一个“app bar”。我将用一个AppBar 控件来允许用户在一个Textbox中输入URL,并且通过点击一个Add按钮来获得应用程序中的源。为了我的GroupedItemsPage.xaml文件,我从Visual Studio 工具栏上拖拽了一个AppBar控件到设计器上:

然后我把生成的XAML文件移动到一个Page.BottomAppBar元素中去,这样这个app bar就能在我的应用程序的底部显示了:

 <Page.BottomAppBar>
     <AppBar>
         <Grid>
             <Grid.ColumnDefinitions>
                 <ColumnDefinition/>
                 <ColumnDefinition/>
             </Grid.ColumnDefinitions>
             <StackPanel Orientation="Horizontal"/>
             <StackPanel Grid.Column="1" HorizontalAlignment="Right" Orientation="Horizontal"/>
         </Grid>
     </AppBar>
 </Page.BottomAppBar>

同时,我加入了三个控件到左对齐的StackPanel:

 <TextBox x:Name="txtUrl" VerticalAlignment="Center" />
 <Button x:Name="btnAddFeed" Style="{StaticResource AddAppBarButtonStyle}" Click="btnAddFeed_Click" />
 <ProgressRing x:Name="prAddFeed" IsActive="false" 
     Foreground="{StaticResource ApplicationForegroundThemeBrush} "/>

(请注意AddAppBarButtonStyle和AppBar按钮的其他140个样式一样都定义在Grid应用程序模板的StandardStyles.xaml文件中,但它是默认注释掉的,我只是取消注释它,这样我可以在这里使用。)

要完成这次体验,我需要执行btnAddFeed_Click 方法(在GroupedItemsPage.xaml.cs文件中),把它连接到我之前写过的方法SampleDataSource.AddGroupForFeedAsync 中(当然,去掉了我之前在LoadState硬编码的三行代码):

    1: async void btnAddFeed_Click(object sender, RoutedEventArgs e)
    2: {
    3:     await AddFeedAsync(txtUrl.Text);
    4: }
    5:  
    6: async Task AddFeedAsync(string feed)
    7: {
    8:     txtUrl.IsEnabled = false;
    9:     btnAddFeed.IsEnabled = false;
   10:     prAddFeed.IsActive = true;
   11:     try
   12:     {
   13:         await SampleDataSource.AddGroupForFeedAsync(feed);
   14:     }
   15:     catch (Exception exc)
   16:     {
   17:         var dlg = new MessageDialog(exc.Message).ShowAsync();
   18:     }
   19:     finally
   20:     {
   21:         txtUrl.Text = string.Empty;
   22:         txtUrl.IsEnabled = true;
   23:         btnAddFeed.IsEnabled = true;
   24:         prAddFeed.IsActive = false;
   25:     }
   26: }

把这个就位以后,当一个用户调出app bar,输入URL,并且点击Add按钮,这个源就会被加入进去,并且在加入操作的过程中,应用程序将显示进度环并阻止用户添加额外的源。

启用Live Tiles

Windows 8 为在开始屏幕上创建live tiles提供了多种机制。就我的应用程序来说,我想要更新我的live tile来列出当前在应用程序中的源。

为了实行这件事,在GroupedItemPage.xaml.cs文件中,我创建了一个函数来生成TileUpdateManager 预期的XML模板,并且我用一个TitleUpdater来推送视觉效果到磁贴中:

    1: private void UpdateTile()
    2: {
    3:     var groups = SampleDataSource.AllGroups.ToList();
    4:     var xml = new XmlDocument();
    5:     xml.LoadXml(
    6:         string.Format(
    7:             @"<?xml version=""1.0"" encoding=""utf-8"" ?>
    8:             <tile>
    9:                 <visual branding=""none"">
   10:                     <binding template=""TileWideText01"">
   11:                         <text id=""1"">News by Soma</text>
   12:                         <text id=""2"">{0}</text>
   13:                         <text id=""3"">{1}</text>
   14:                         <text id=""4"">{2}</text>
   15:                     </binding>
   16:                     <binding template=""TileSquarePeekImageAndText01"">
   17:                         <image id=""1"" src=""ms-appx:///Assets/Logo.png"" alt=""alt text""/>
   18:                         <text id=""1"">News by Soma</text>
   19:                         <text id=""2"">{0}</text>
   20:                         <text id=""3"">{1}</text>
   21:                         <text id=""4"">{2}</text>
   22:                     </binding>  
   23:                 </visual>
   24:             </tile>", 
   25:             groups.Count > 0 ? groups[0].Title : "", 
   26:             groups.Count > 1 ? groups[1].Title : "",
   27:             groups.Count > 2 ? groups[2].Title : ""));
   28:     TileUpdateManager.CreateTileUpdaterForApplication().Update(new TileNotification(xml));
   29: }

(在你自己的应用程序中,“App tiles and badges”Windows SDK示例包含了一些有用的代码来和磁贴一同使用)。

然后我修改了GroupedItemsPage.LoadState来在成功加入源之后调用UpdateTitle方法:

    1: if (await SampleDataSource.AddGroupForFeedAsync(feed))
    2: {
    3:     UpdateTile();
    4: }

现在,在开始我的应用程序以及加入我的博客源Jason Zander的博客源之后,我在我的磁贴上看到这些信息了:

启用搜索

在与live tiles集成了之后,下一个我想集成的功能是搜索。Windows 8中提供了搜索的魅力,使用户能够从系统中的任何地方搜索相关的应用程序。

开始,我在Visual Studio中右击我的项目,并选择Add | New Item…,选择“Search Contract”作为要被加入的项:

这完成了一些事情:

  • 它更新了我的清单来声明搜索:

  • 它加入了一个新的SearchResultsPage.xaml到我的项目中。
  • 它用必要的OnSearchActivated 方法重写增强了我的App.xaml.cs文件,来正确地将我的新SearchResultsPage.xaml文件与系统中的搜索请求相连接。

被加入的SearchResultsPage.xaml(和相关的SearchResultsPage.xaml.cs)已经包含了大部分的UI和必要的逻辑来让这个方案工作。所以,通过其他的模板我们已经看到了Visual Studio的创建,我只需要把逻辑插入到我的应用程序和数据中去。

这个SearchResultsPage.xaml.cs文件包括一个简单的视图模型类型,称为Filter。这个类型是被模板用来代表一组搜索结果,这样,用户可以查看和从多种类型结果中选择,而快速缩小他们的选择范围。为了使编程简单一些,我首先修改了这个Filter使它成为Filter<T>,并且给它加入一个Results属性。这样,我可以执行一个搜索,并填充所有的过滤器,这样,当用户在UI中选择不同的过滤器类别时,我不需要不停地搜索:

    1: private sealed class Filter<T> : News_by_Soma.Common.BindableBase
    2: {
    3:     ...
    4:     private List<T> _results;
    5:  
    6:     public Filter(string name, IEnumerable<T> results, bool active = false)
    7:     {
    8:         ...
    9:         this.Results = results.ToList();
   10:     }
   11:  
   12:     public int Count { get { return _results.Count; } }
   13:  
   14:     public List<T> Results
   15:     {
   16:         get { return _results; }
   17:         set { if (this.SetProperty(ref _results, value)) this.OnPropertyChanged("Description"); }
   18:     }
   19:     ...
   20: }

在页面的LoadState重写中,我把模板里提供的硬编码的搜索结果过滤组:

    1: var filterList = new List<Filter>();
    2: filterList.Add(new Filter("All", 0, true));

用实际在每个RSS源中搜索的代码替换了,给每个源创建一个过滤器,然后创建一个“All”过滤器聚集所有的结果:

    1: var filterList = new List<Filter<SampleDataItem>>(
    2:     from feed in SampleDataSource.AllGroups
    3:     select new Filter<SampleDataItem>(feed.Title,
    4:         feed.Items.Where(item => (item.Title != null && item.Title.Contains(queryText)) || 
    5:                                      (item.Content != null && item.Content.Contains(queryText))),
    6:         false));
    7: filterList.Insert(0, 
    8:     new Filter<SampleDataItem>("All", filterList.SelectMany(f => f.Results), true));

接下来,在Filter_SelectionChanged中,我把结果储存到DefaultViewModel:

    1: this.DefaultViewModel["Results"] = selectedFilter.Results;

然后,我加入了一个ItemClick事件处理程序到模板的resultsListView控件中。当用户点击的时候,它能导航到所选择的项:

    1: private void resultsListView_ItemClick(object sender, ItemClickEventArgs e)
    2: {
    3:     var itemId = ((SampleDataItem)e.ClickedItem).UniqueId;
    4:     this.Frame.Navigate(typeof(ItemDetailPage), itemId);
    5: }

最后,SearchResultsPage.xaml页面包含两行XAML代码,它们应该被删除掉。Grid应用程序模板在App.xaml中已经包含了应用程序的名字,所以我们不需要也在这里配置它们:

<!-- TODO: Update the following string to be the name of your app -->

<x:String x:Key="AppName">App Name</x:String>

就这样,搜索在我的应用程序中能够运作了:

既然如此,当应用程序的主页面加载的时候,它只会搜索被加入到应用程序中的源。如果应用程序正在运行,执行搜索能够很好的起作用。但是,如果应用程序没有运行,主页就不会执行,并且组就不会被填充。如果我要保存源信息,然后如果当搜索请求到达时,应用程序没有被运行,我可以更新搜索逻辑来首先加载被保存的源。

接下来要做什么?

在这两篇博客中的第一篇,我已经探讨了启动和运行使用Visual Studio Express 2012 for Windows 8,以及用它来创建一个基本的应用程序,它集成了live tiles和搜索。在我的下一篇博客中,我会侧重于通过多后端服务拓展这个应用程序。敬请期待。

致敬!