65.9K
CodeProject 正在变化。 阅读更多。
Home

蓝色时刻

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (60投票s)

2012年3月27日

CPOL

16分钟阅读

viewsIcon

97963

downloadIcon

1754

本文介绍Blue Hour的开发,这是一款Windows Phone 7应用程序,用于计算您所在位置的日出和日落时间。

SplashScreen Blue Hour Screenshot Blue Hour

目录

引言

本文描述了Blue Hour的实现和开发过程。Blue Hour是一款Windows Phone应用程序,用于计算未来30天的日出和日落时间。

我喜欢用数码相机拍照。正如大多数摄影师所知,光线是照片中最重要的元素。日出或日落时的光线尤其美丽,使得这些时段成为理想的拍照时刻。

蓝色时光(Blue hour)指的是早晚两次晨昏的暮光时段,既非白昼也非完全黑暗。由于此时的光线品质,这段时间被认为非常特别,因此应用程序也因此得名Blue Hour。

背景

在2012年荷兰Techdays期间,微软向与会开发者提供了以下优惠:如果你为Windows Phone创建三个应用程序,并且这些应用程序在应用商店获得批准,你将获得一部Nokia Lumia 800。我没有立即申请,因为我不知道需要花费多少时间。两天后,我决定申请这项优惠,因为我喜欢挑战。描述了我三个想法后,我收到了一部新Nokia Lumia 800手机。所以是时候创建我的第一个Windows Phone应用程序了。

本文写于我第二次提交第一个应用到应用商店之后。第一次提交未能通过认证,原因将在文章后面详述。由于我计划开发三个应用程序,我花了一些时间来创建开发的基础设施。

基础设施和工具

Windows Phone应用程序使用Microsoft Silverlight开发。我使用Visual Studio来实现应用程序。安装Windows Phone SDK时,Visual Studio Express会自动安装。

关于如何创建Windows Phone应用程序的说明和文档,我使用了优秀的教程应用程序TailSpin,该教程描述了微软的一个软件开发团队如何实现Windows phone应用程序。我认为这是一个真实的实现,而不是典型的演示应用程序。其他使用的资源包括channel 9的视频以及MSDN上的文档

在Blue Hour的实现过程中,使用了以下框架和模式。

Blue Hour的功能

如第一段所述,Blue Hour为您的当前位置计算未来30天的日出和日落时间。该应用程序通过Windows Phone的位置服务检测您的位置。我将在本文后面详细介绍这一点。日出和日落的计算是从先前应用程序的源代码中重用的。源代码需要转换为Silverlight程序集,这个过程没有遇到任何问题。

该应用程序有以下要求

  • 在可滚动列表框中清晰地显示时间
  • 允许用户选择是否允许使用位置服务
  • 启用评分、分享和通过电子邮件联系
  • 跟踪应用程序的使用情况

架构

由于我计划编写三个应用程序,我花了一些时间思考这三个应用程序的基础架构。每个应用程序都将使用MVVM模式来分离视图和业务逻辑。依赖注入用于将类的实例创建与类的实际使用分离开来。NFunq依赖注入框架执行实例的实际注入。

structure

数据如何被检索并可视化到视图上,最好用一个例子来描述。上图显示了在此过程中起作用的类。左边的类是视图,而右边的类负责从外部或内部服务检索数据。

视图SunriseSunsetListViewSunriseSunsetListViewModel进行数据绑定。当用户在视图上按下刷新按钮时,SunriseSunsetListViewModel中的一个命令被触发。SunriseSunsetListViewModel调用AstronomyService来检索日出日落时间列表。AstronomyService反过来使用LocationServiceSunCalculator来获取当前位置并计算该位置的日出日落时间。SunriseSunset列表AstronomyService充当模型。这个模型由SunriseSunsetListViewModel映射到ViewModel。您可以创建一个单独的类来执行此映射,由于应用程序的规模和范围有限,我决定不实现它。

NFunq和MVVM

Blue Hour使用MVVM模式将表示层与业务逻辑分离。应用程序中的所有ViewModel都通过数据绑定绑定到视图。实现了一个特殊的类ViewModelLocator,它包装了依赖项容器。ViewModelLocator为应用程序中的每个ViewModel都有一个单独的属性。下面是ViewModelLocator类的一个摘录。

public class ViewModelLocator
{
  private readonly ContainerLocator containerLocator;

  public ViewModelLocator()
  {
    this.containerLocator = new ContainerLocator();
  }

  public SunriseSunsetListViewModel SunriseSunsetListViewModel
  {
    get
    {
      return this.containerLocator.Resolve();
    }
  }
  ....

ViewModelLocator类在构造函数中实例化ContainerLocator。ContainerLocator包装了NFunq的Container类。SunriseSunsetListViewModel从ContainerLocator请求SunriseSunsetListViewModel类的实例。然后,在XAML中使用视图的DataContext通过静态资源将此ViewModel与视图关联起来。要实现这一点,必须在App.xaml文件中创建资源。

<Application.Resources>
  <viewmodels:ViewModelLocator x:Key="ViewModelLocator"/>
</Application.Resources>

然后,可以在视图中使用该资源,如下所示将视图绑定到ViewModel。

DataContext="{Binding Source={StaticResource ViewModelLocator}, Path=SunriseSunsetListViewModel}"

ContainerLocator类包装了NFunq的Container类,它包括类型的注册。通用的Resolve方法负责构造所请求类型的实例。该方法将实例的实际创建委托给Funq容器。此类将应用程序与实际的DI实现分开。这样,如果将来需要,我们就可以自由地切换到另一个DI框架。

public class ContainerLocator : IDisposable
{
  private readonly Container container;

  public ContainerLocator()
  {
    this.container = new Container();
    this.ConfigureContainer();
  }

  public TService Resolve()
  {
    return container.ResolveNamed((string)null);
  }

  private void ConfigureContainer()
  {
    this.container.Register(
        new SunriseSunsetListViewModel(
            new AstronomyService(new SunCalculator(), new LocationService(
                new BlueHourSettingsManager(new SettingsHelper()))), new BlueHourSettingsManager(new SettingsHelper())));
    this.container.Register(new SettingsViewModel(new BlueHourSettingsManager(new SettingsHelper())));
    this.container.Register(new AboutViewModel());
  }
....	

曾经在Silverlight项目中将Prism框架与Unity一起使用的用户可能会觉得实现方式很奇怪。Funq没有自动注册扫描程序集以查找类型并注册它们。不支持与依赖项的视图的自动构造。我没有为Windows Phone找到其他支持这些功能的DI容器。

Prism

我在Silverlight开发中使用过Prism库,因此很高兴发现有Windows Phone版本。选择这个框架的最大原因在于,你可以轻松地将命令附加到视图并触发全局事件。使用MVVM模式,你不会直接在例如按钮的事件处理程序中编写代码。相反,你会在ViewModel中创建一个Command,并通过普通的databinding将按钮绑定到这个Command。

这样做的好处是,你的视图现在与ViewModel完全解耦。这使得你可以更轻松地测试你的ViewModel。

public DelegateCommand AboutCommand
{
  get
  {
    return aboutCommand;
  }

  set
  {
    aboutCommand = value;
    OnPropertyChanged("AboutCommand");
  }
}

public SunriseSunsetListViewModel(IAstronomyService astronomyService, BlueHourSettingsManager blueHourSettingsManager)
{
  this.AboutCommand = new DelegateCommand(() => NavigationService.Navigate("/Views/About.xaml"));
  ...

这个DelegateCommand在Silverlight中不可用,它是Prism框架自带的功能。在ViewModel的构造函数中,我创建了一个DelegateCommand的实例,并指示用户按下按钮时导航到关于视图。

<Custom:Interaction.Behaviors>
	<prismInteractivity:ApplicationBarButtonCommand ButtonText="Settings" CommandBinding="{Binding ShowSettingsCommand}"/>
	<prismInteractivity:ApplicationBarButtonCommand ButtonText="Refresh" CommandBinding="{Binding RefreshCommand}"/>
	<prismInteractivity:ApplicationBarButtonCommand ButtonText="About" CommandBinding="{Binding AboutCommand}"/>
</Custom:Interaction.Behaviors>

命令在XAML中绑定到ApplicationBarCommand。

单元测试

可以使用Windows Phone的Silverlight单元测试框架进行单元测试。这个框架是从原始的Silverlight单元测试框架移植过来的。你需要一个单独的Windows Phone应用程序来执行实际的测试。虽然这不是理想的,因为它容易打断你的TDD节奏,但总比没有好。

Jeff Wilcox在他的博客上描述了这个框架的发布。我只是将计算日出日落时间的类的现有测试用例转换为使用这个框架。

动画

Windows Phone的Silverlight工具包包含了一个很好的页面或视图过渡的实现。与其使用普通的“无聊”页面过渡,不如在屏幕之间导航时创建滑动或旋转动画。在你的应用程序中实现这个非常简单。首先你需要引用该工具包。设置好引用后,你需要在App.xaml文件中的Application.Resources元素中包含以下样式。

<Style x:Key="AnimatedPage" TargetType="phone:PhoneApplicationPage">
  <Setter Property="toolkit:TransitionService.NavigationInTransition">
    <Setter.Value>
      <toolkit:NavigationInTransition>
        <toolkit:NavigationInTransition.Backward>
          <toolkit:SlideTransition Mode="SlideRightFadeIn"/>
        </toolkit:NavigationInTransition.Backward>
        <toolkit:NavigationInTransition.Forward>
          <toolkit:SlideTransition Mode="SlideLeftFadeIn"/>
        </toolkit:NavigationInTransition.Forward>
      </toolkit:NavigationInTransition>
    </Setter.Value>
  </Setter>
  <Setter Property="toolkit:TransitionService.NavigationOutTransition">
    <Setter.Value>
      <toolkit:NavigationOutTransition>
        <toolkit:NavigationOutTransition.Backward>
          <toolkit:SlideTransition Mode="SlideRightFadeOut"/>
        </toolkit:NavigationOutTransition.Backward>
        <toolkit:NavigationOutTransition.Forward>
          <toolkit:SlideTransition Mode="SlideLeftFadeOut"/>
        </toolkit:NavigationOutTransition.Forward>
      </toolkit:NavigationOutTransition>
     </Setter.Value>
  </Setter>
</Style>	

对于我的应用程序,我选择了滑动过渡,但还有其他过渡可用。当你引用页面中的这个样式时,导航到另一个页面时会发生过渡。应该在页面的XAML中添加以下内容。 

  <phone:PhoneApplicationPage 
    Style="{StaticResource AnimatedPage}"

你还需要将代码隐藏中的RootFrame实例更改为TransitionFrame

// Do not add any additional code to this method
private void InitializePhoneApplication()
{
  if (phoneApplicationInitialized)
    return;

  // Create the frame but don't set it as RootVisual yet; this allows the splash
  // screen to remain active until the application is ready to render.
  RootFrame = new TransitionFrame();
  RootFrame.Navigated += CompleteInitializePhoneApplication;

  // Handle navigation failures
  RootFrame.NavigationFailed += RootFrame_NavigationFailed;

  // Ensure we don't initialize again
  phoneApplicationInitialized = true;
}

导航 

NavigationService是一个默认的Windows Phone类,负责在页面之间导航。由于Blue Hour使用MVVM模式,它无法直接使用导航服务。NavigationService是Page类的一个属性,而ViewModel对视图一无所知。

有几种解决方案可以解决这个问题;例如,一种方法是将NavigationService从页面传递到ViewModel。但这会使ViewModel的测试更加困难。因此,我使用了Rob Garfoot描述的解决方案。在此解决方案中,他创建了一个NavigationService的包装器,该包装器通过依赖属性进行填充。

使用这个解决方案,如果您遵循以下三个步骤,就可以在ViewModel中进行导航。

  1. 将以下内容添加到想要导航的页面的XAML中。
    <phone:PhoneApplicationPage 
        Navigation:Navigator.Source="{Binding}"
    
  2. 在ViewModel中实现INavigable,以便依赖对象可以设置ViewModel的NavigationService。
  3. 使用以下代码导航到另一个视图。
    private void NavigateToAView()
    {
      NavigationService.Navigate("Views/About.xaml");
    }      
    

存储设置

大多数应用程序都包含一些用户可以更改的设置。Blue Hour有一个设置,允许用户明确选择是否允许应用程序使用手机的位置服务。一个单独的视图负责管理这些设置。Windows Phone平台使用Silverlight的独立存储来持久化设置。

public class SettingsHelper
{
  private readonly IsolatedStorageSettings settings = 
      IsolatedStorageSettings.ApplicationSettings;

  public T GetSetting(string settingName, T defaultValue)
  {
    if (!settings.Contains(settingName))
    {
        settings.Add(settingName, defaultValue);
    }

    return (T)settings[settingName];
  }

  public void UpdateSetting(string settingName, T value)
  {
    if (!settings.Contains(settingName))
    {
        settings.Add(settingName, value);
    }
    else
    {
        settings[settingName] = value;
    }

    settings.Save();
  }
}

为了读取和保存设置,我创建了SettingsHelper类。这个类封装了读取和写入Windows Phone独立存储的操作。使用此类很简单,只需提供设置的名称和值,具体取决于您是要写入还是读取设置。

public bool IsLocationServiceAllowed
{
  get
  {
    return this.settingsHelper.GetSetting(
      Constants.Settings.AllowAccessToLocationServicesKey, false); 
  }

  set
  {
      this.settingsHelper.UpdateSetting(
        Constants.Settings.AllowAccessToLocationServicesKey, value);  
  }
}  

ViewModel中的此IsLocationServiceAllowed属性将绑定到ToggleSwitch,以便用户可以调整设置。

位置服务

Blue Hour使用Windows Phone的位置服务来检测客户的位置,并根据该位置计算日出和日落时间。通过GeoCoordinateWatcher类可以访问位置服务。此类构造函数接受一个参数,该参数指定应用程序所需位置的精度。

private readonly GeoCoordinateWatcher geoCoordinateWatcher = 
  new GeoCoordinateWatcher(GeoPositionAccuracy.Default);

构造函数的精度参数可以有两个可能的值:GeoPositionAccuracy.Default,表示低精度,和GeoPositionAccuracy.High,显然表示高精度。Blue Hour只需要低精度,这可能意味着它提供坐标更快。

本地化

我希望Blue Hour应用程序支持两种语言:英语和荷兰语(我的母语),因此该应用程序需要本地化。与所有需要本地化的Microsoft .Net应用程序一样,它首先要将所有本地化字符串添加到资源文件中。

BlueHour的默认语言是英语,因此AppResources.resx文件中的键值对是英语。所有其他支持的语言都应按照以下文件名格式添加:AppResources.[culture].resx。应用程序支持的区域设置应在应用程序的项目文件中指定。这有点麻烦,您必须在Visual Studio中卸载项目并手动编辑项目XML。应用程序中的SupportedCultures元素应包含应用程序支持的语言。多个区域设置可以通过分号分隔。

  
    nl-NL; en-US;
  

通过将应用程序支持的区域设置添加到项目文件中,当您通过“区域+语言”页面更改手机的显示语言时,应用程序将自动切换到该区域设置。

Change Regional Language

为了在源代码中引用资源,我引入了一个新类LocalizedStrings,它从Xaml视图中使用。

public class LocalizedStrings
{
  private readonly AppResources localizedResources = new AppResources();

  public AppResources LocalizedResources
  {
    get
    {
        return localizedResources;
    }
  }
}  

您必须创建一个应用程序资源,才能通过数据绑定使用此类/资源。以下内容将LocalizedStrings类添加到应用程序资源中。

<Application.Resources>
  <local:LocalizedStrings x:Key="LocalizedStrings" />
</Application.Resources>  

添加此项后,可以使用以下标准数据绑定语法绑定到资源。Text="{Binding LocalizedResources.ApplicationTitle, Source={StaticResource LocalizedStrings}}"定义了实际的资源键(LocalizedResources.ApplicationTitle)以及它的来源Source={StaticResource LocalizedStrings}

<TextBlock x:Name="ApplicationTitle" 
    Text="{Binding LocalizedResources.ApplicationTitle, Source={StaticResource LocalizedStrings}}" 
    Style="{StaticResource PhoneTextNormalStyle}"/>

在代码中引用资源可以使用正常的.Net方式,即通过引用AppResource.ApplicationTitle

请注意,切换到另一种语言的唯一方法是重启手机。如果您想让用户动态更改应用程序的语言,则必须在应用程序中实现这一点,通过使用语言切换器。 Joost van Schaik详细介绍了如何在您的应用程序中实现这一点。

您的应用的营销

如果您是独立应用开发者,您将不得不自己进行应用程序的营销。通过社交媒体和应用商店的集成,这变得可能甚至很简单。

评分

评分是让您的应用更具可见性的最重要的方面之一。通过良好的评分,您的应用会变得更具可见性,更多的可见性带来了获得更多好评分的机会。评分可以通过应用商店为应用程序提供。通过让用户轻松地获得评价您应用程序的能力,用户更有可能评价您的应用程序。在BlueHour中,我添加了一个单独的“评分”按钮,直接将用户重定向到评价您应用的页面。实现这一点非常容易。

private void RateApp()
{
  new MarketplaceReviewTask().Show();
}

这会将用户直接带到您的应用的评价页面。您甚至可以向用户显示一个弹出窗口,并在每次启动应用程序时询问他们是否评价该应用程序。但我发现这种行为非常令人讨厌。

社交媒体

营销您应用程序的另一种可能性是利用社交媒体为您应用制造话题。Windows Phone能够通过用户已连接的社交媒体分享各种内容。在您的应用程序中实现此功能也非常容易。

private void ShareApp(SunriseSunset sunriseSunset)
{
  var shareTask = new ShareLinkTask();
  shareTask.Message = string.Format(
      "According to BlueHour the sun rises at {0} and sets at {1} tomorrow.", 
      sunriseSunset.Sunrise, 
      sunriseSunset.Sunset);
  shareTask.Title = "Blue Hour WP7 App";
  shareTask.LinkUri = new Uri("http://www.semanticarchitecture.net");
  shareTask.Show();
}  

当用户按下执行此代码的按钮时,用户可以选择在哪个已链接的社交媒体帐户上共享该消息。然后,在请求确认后,消息将被发送。

支持

您的应用程序的用户可能会遇到应用程序中的错误。用户报告此错误最明显的方式是使用应用商店中的评分机制。很可能这将是一个负面评分。您无法联系创建负面评分的用户。为了支持遇到麻烦的用户,您可以在应用程序中集成支持选项。

public void ContactAuthor()
{
  var emailTask = new EmailComposeTask();
  emailTask.Subject = "#BlueHour support request";
  emailTask.To = "pkalkie@gmail.com";
  emailTask.Show();
}

在Blue Hour中,我添加了用户向我发送支持电子邮件的功能。上面的代码将创建一个电子邮件模板并在发送电子邮件对话框中显示。如果用户愿意,他或她可以联系我作为应用程序的作者寻求支持。

更多应用

您还可以通过推荐您可能已实现并且在应用商店中可用的其他应用程序来吸引用户的注意力。这将推广您在应用商店中提供的其他应用程序。

public void MoreApps()
{
  var searchTask = new MarketplaceSearchTask();
  searchTask.SearchTerms = "Patrick Kalkman";
  searchTask.Show();
}

SearchTerms属性应设置为您的发布者名称,以启用搜索您的其他应用程序。

移动分析

应用商店会跟踪下载您应用程序的客户数量。但是您看不到客户是否以及多久使用您的应用程序。有几种选项可为您的应用程序添加分析功能。以下是一些比较受欢迎的选项。

对于Blue Hour,我选择了mTiks平台,因为它易于实现且免费使用。要实现它,您需要执行以下操作。

  • mTiks注册
  • 在mTiks注册您的应用程序并将其添加到您的帐户
  • 复制您的应用程序的标识
  • 下载Windows Phone mTiks程序集
  • 引用mTiks程序集

以下内容应添加到您的app.xaml文件的代码隐藏中。这会通知mTiks何时启动和停止您的应用程序。

private void Application_Launching(object sender, LaunchingEventArgs e)
{
  mtiks.Instance.Start(MTiksApplicationKey, Assembly.GetExecutingAssembly());
}

private void Application_Activated(object sender, ActivatedEventArgs e)
{
  mtiks.Instance.Start(MTiksApplicationKey, Assembly.GetExecutingAssembly());
}

private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
  mtiks.Instance.Stop();
}

private void Application_Closing(object sender, ClosingEventArgs e)
{
  mtiks.Instance.Stop();
}  

您还可以通过触发事件来通知mTiks用户在应用程序中执行某个函数。

mtiks.Instance.postEventAttributes("REFRESH");

mTiks随后会记录您的应用程序触发该事件的次数。一旦完成集成,mTiks就会为您提供一个包含您应用分析信息的漂亮仪表板。

认证应用

当您完成应用程序并希望将其分发给客户时,该应用程序必须获得微软的认证。在应用程序获得认证之前,它会经过彻底的测试。为了准备认证,您可以使用Market Place Test Kit,该工具包可以通过右键单击主应用程序并选择“Open Marketplace Test Kit”来启动。该测试套件不言自明,包括一些自动化测试和一些手动测试,对应于微软在应用程序认证前执行的测试。

您需要更新的一件事是WMAppManifest.xml文件中的功能部分,该文件包含在您的项目中。此部分描述了应用程序从手机基础架构所需的功能。确保用所需的功能更新此部分。Market Place Test Kit能够识别应用程序所需的功能,并可以将它们写入WMAppManifest.xml文件。

隐私政策

正如我在引言中所述,本文在我第二次提交Blue Hour到应用商店后不久写成的。我第一次提交应用时,它未能通过认证。从测试团队收到的测试报告表明,我没有包含隐私政策。 

我忽略了这一点,因为它在要求中已清楚说明。2.7.2规定,“您的应用程序的隐私政策必须告知用户位置数据如何被使用和披露,以及用户对位置数据的控制。这可以托管在应用程序内部或直接从应用程序链接。”

我所做的是在设置屏幕上添加了一个描述,说明应用程序如何处理检索到的位置数据,以及用户可以启用或禁用它。

我从源代码中删除了某些部分,例如我的mtiks框架的应用程序密钥。Blue Hour的源代码可供下载,希望能让您开发第一个Windows Phone应用程序更容易一些。

结论  

该应用程序可在应用商店中找到,完整的源代码可从文章顶部下载。如果您喜欢这篇文章,欢迎投票或评论。谢谢。  

下面展示了一张我在蓝色时光拍摄的照片。摄于荷兰鹿特丹。 

历史 

  • v1.0 2012/03/27: 初始发布  
  • v1.1 2012/03/30: 对文章中关于认证的内容进行小幅更新 
  • v1.2 2012/04/23: 添加了蓝色时光拍摄的照片 
© . All rights reserved.