Bing Map 应用程序

使用 Bing Map 应用程序 SDK 构建实时传输应用程序

Luan Nguyen

下载代码示例

六月份的 Tech•Ed 2010 大会上,Microsoft 发布了免费的 Bing Map 应用程序 SDK,开发人员可以用它在位于 bing.com/maps/explore/ 的 Bing Maps 探讨站点上编写以地图为中心的应用程序。

这为各种组织、公司或爱好者提供了大量的机会,可以在 Bing Maps 中创建自己的地图绘制体验。对于公司而言,可以编写应用程序用于宣传产品或完善其在线服务。例如,bestparking.com 开发了一个“停车查找器”应用程序,可以帮助您使用其数据库查找所在城市的所有停车场。

如需查看地图应用程序的外观,请转到 Bing Maps 探讨站点,然后单击页面左下角附近的“MAP APPS”按钮。此时将打开地图应用程序库,其中显示可用的应用程序。这个库中已经拥有了众多应用程序,并且数量仍在增长。

在本文中,我将介绍使用 Bing Map 应用程序 SDK 编写地图应用程序所需执行的操作。我将引导您完成示例应用程序的开发,该应用程序显示华盛顿州金恩郡的公交车到站实时信息。此应用程序的灵感源自热门的 One Bus Away 应用程序。

最终产品

在开始研究代码之前,我们先了解一下最终地图应用程序的外观。在激活后,如果将地图缩放到金恩郡地区,则该地图将在左侧面板中显示地图视距内所有公交车站列表(请参见图 1)。

图 1 OBA 应用程序在左侧面板中显示公交车信息,在地图上显示图钉

每个公交车站上提供了该站停靠的所有公交车的相关信息。单击各个车站的“到站时间”超链接可以打开第二级面板,其中显示即将到站的到站时间(请参见图 2)。

图 2 特定公交车站的公交车到站时间

该应用程序还为地图上的每个公交车站添加了醒目的图钉(以字母 B 标记)。您可以与图钉交互以激活弹出窗口,在该窗口中可以针对特定公交车站调用常用命令,如获取该车站往来方向的驾驶或步行路线(请参见图 3)。

图 3 单击图钉可显示提供附加信息的弹出 UI

下载 Bing Map 应用程序 SDK

在首次开始编写地图应用程序之前,您需要从 connect.microsoft.com/bingmapapps 安装 Bing Map 应用程序 SDK。该 SDK 提供了引用程序集、一个示例项目和脱机文档。此外,由于 SDK 基于 Silverlight 4,您还需要安装 Silverlight 4 Tools for Visual Studio 2010,可从 silverlight.net/getstarted/ 下载。

地图应用程序基础知识

Bing Maps 探讨站点在构建之时便将可扩展性作为最高优先级。Bing Maps 小组希望能使全球开发人员方便地增加对其有用以及可能会为其他人提供帮助的功能。在这个目标下,地图应用程序 是一种利用 Bing Maps 核心部分提供的构建块服务的具体功能。在默认的 Bing Maps 站点中,行车路线、企业搜索和位置搜索功能均构建自相同的构建块。

考虑到这一目标,Bing Maps 小组决定构建可扩展框架来支持地图应用程序的概念。在这个框架上编写时,地图应用程序利用了具有依赖关系注入样式的方法提供对功能的访问,这与 Managed Extensibility Framework (MEF) 类似。

该框架的核心部分是插件的概念。插件是一种扩展单元,允许开发人员以可维护的分离方式增加功能。在 MEF 术语中,可以将其看作等同于一个可组合部分。插件使用属性来声明所需的导入以及要共享的导出。在运行时,框架将解析插件之间的依赖关系,并将具有相同约定的导入和导出匹配。

要编写地图应用程序,您可以创建 Silverlight 4 类库,其中包含且只包含一个基本插件类的子类。为了执行有用的操作,插件有可能导入多种内置约定,以便您访问不同核心服务。SDK 文档中有一部分专门列出了由框架导出的所有内置约定。

要引用插件类和所有内置约定类型,您的项目需要从 SDK 引用提供的程序集。图 4 简要介绍了这四种程序集。

图 4 地图应用程序项目的引用程序集

程序集名称 说明
Microsoft.Maps.Plugins.dll 包含基本插件类和相关的导入/导出属性类。
Microsoft.Maps.Core.dll 包含 Bing Maps 提供的所有约定类型。
Microsoft.Maps.MapControl.Types.dll 包含处理地图控件所需的类型。
Microsoft.Maps.Extended.dll 包含与 StreetSide 地图模式交互的类型。

访问 One Bus Away API

为了获取实时的公交车到站信息,我利用了对公共开放的 One Bus Away REST API(下文中简称为 Oba API)。有关 Oba API 的详细信息,请参见 code.google.com/p/onebusaway/wiki/OneBusAwayRestApi。(请注意,该 API 当前可免费用于非商业用途,但必须先注册应用程序密钥才可访问。在本文的可下载代码中,我删除了分配给我的密钥,因此,如果您想自行编译和试用应用程序,则需要用自己的密钥替换。)

在 Silverlight 应用程序尝试访问其他域上的 Web 服务时,Silverlight 运行时要求必须显式选择采用服务,以允许跨域访问。服务在承载服务的域的根处放置 clientaccesspolicy.xml 或 crossdomain.xml 文件,用于指示其许可。有关这两个文件方案的更多详细信息,请参见 msdn.microsoft.com/library/cc197955%28VS.95%29。幸运的是,Oba API 提供了 crossdomain.xml 文件,这使得我的地图应用程序可以在 Silverlight 代码中调用它。

在可下载的解决方案中,您可以看到两个库对象:ObaApp 和 ObaLib。ObaApp 是主项目,其中包含地图应用程序插件并引用 ObaLib。ObaLib 是另一个类库项目,其中封装了所有帮助程序类,用于与 Oba API 通信。我将其做成了一个单独的库,需要时可以在不同项目中方便地重用。本文并不详细介绍此库中的每个类,但建议您查看源代码以了解此处各个类的详细信息。

要了解的最重要的类是 ObaSearchHelper 类,该类提供了用于对 Oba API 进行查询的便利方法和事件:

public sealed class ObaSearchHelper
{
  public void SearchArrivalTimesForStop(string stopId);
  public event EventHandler<BusTripsEventArgs>
    SearchArrivalTimesForStopCompleted;


  public void SearchStopsByLocation(double latitude, double longitude, 
    double radius, int maxResultCount);
  public event EventHandler<BusStopsEventArgs> 
    SearchStopsByLocationCompleted;

  // more method/event pairs
  // ...
}

在此类中可以方便地找出所用模式。 对于 Oba API 的每个 REST 端点,存在一个方法可用于触发搜索,而另一个对应事件用于通知该搜索完成。 事件的订阅者可以从事件参数对象获取结果。

在准备好这个便利的类之后,我将逐一说明 ObaApp 项目中的主类。

定义地图实体

编写地图应用程序时,要做的第一件事情是定义地图实体。 下面是来自 SDK 文档的实体定义:“实体是地图上存在地理关联的项目。 地图实体可以是点、折线、多边形或图像覆盖。”一条简单的经验法则是,如果要在地图上放置某物,则需要一个实体的实例来表示该物。 通常,您需要创建实体的子类来为实体添加附加属性和自定义逻辑。 在我的应用程序中,由于希望显示公交车站的位置,我编写了 BusStopEntity 类来表示公交车站。 图 5 显示了 BusStopEntity 类。

图 5 BusStopEntity 类

public class BusStopEntity : Entity
{
  private readonly DependencyProperty NamePropertyKey =
    Entity.RetrieveProperty(“Name”, typeof(string));

  private BusStop _busStop;

  // Short name of this bus stop
  public string Name
  {
    get { return _busStop.Name; }
  }

  // Unique id of this bus stop
  public string StopId
  {
    get { return _busStop.Id; }
  }

  public BusStopEntity(BusStop busStop, 
    PushpinFactoryContract pushpinFactory)
  {
    _busStop = busStop;

    // Set the location of this Entity
    Location location = 
      new Location(_busStop.Latitude, _busStop.Longitude);
    this.Primitive = pushpinFactory.CreateStandardPushpin(location, “B”);

    // Set the name of this Entity
    this.SetValue(NamePropertyKey, _busStop.Name);
  }
}

我的 BusStopEntity 类公开了两个属性:Name 和 StopId,稍后这两个属性将用于对 UI 控件的数据绑定。 这些值来自基础 BusStop 实例。 BusStop 类在 ObaLib 项目中定义,封装了公交车站的数据,这些数据获取自 Oba API。

在构造函数中,我还设置了 Primitive 和 Name 属性。 Primitive 属性表示实体的类型(例如,点、多边形或折线)和位置。 我将位置设置为 BusStop 实例的经度和纬度值。 此外,将 Primitive 属性设置为 PushpinFactoryContract.CreateStandardPushpin 的返回值可以让我的实体具有标准 Bing Maps 图钉的外观和使用感受。 PushpinFactoryContract 类型提供了创建通用图钉 UI 元素的便利方法。 如果您希望创建自定义的图钉形状,则无需使用它。 此处我只是在图钉内部放置了字母 B(代表公交车),而您可以使用图像或任意 UIElement。

虽然我在子类中定义了 Name 属性,但 SDK 中的其他类型不会识别该属性,这些类型对我的属性一无所知。 因此,我还将相同的值设置给通过调用 Entity.RetrieveProperty(“Name”, typeof(string)) 检索到的 Name 依赖关系属性。 采用这种方法,使得其他功能可以检索公交车站实体的名称。

编写主插件

如前所述,您需要一个插件派生的类以表示地图应用程序。 图 6 显示了我的名为 ObaPlugin 的插件。 我的插件总共导入六个约定。 请注意,由 Bing Maps 提供的所有约定都遵守 Microsoft 的命名约定/*。 SDK 文档提供了可以导入的所有约定的详细说明,如图 7 中所示。

图 6 在 ObaPlugin 类中声明的导入

public class ObaPlugin : Plugin
{

  #region Imports

  [ImportSingle(“Microsoft/LayerManagerContract”, ImportLoadPolicy.Synchronous)]
  public LayerManagerContract LayerManager { get; set; }

  [ImportSingle(“Microsoft/PopupContract”, ImportLoadPolicy.Synchronous)]
  public PopupContract PopupContract { get; set; }

  [ImportSingle(“Microsoft/ConfigurationContract”, 
    ImportLoadPolicy.Synchronous)]
  public ConfigurationContract ConfigurationContract { get; set; }

  [ImportSingle(“Microsoft/MapContract”, ImportLoadPolicy.Synchronous)]
  public MapContract Map { get; set; }

  [ImportSingle(“Microsoft/PushpinFactoryContract”, 
    ImportLoadPolicy.Synchronous)]
  public PushpinFactoryContract PushpinFactory { get; set; }

  [ImportSingle(“Microsoft/ContributorHelperContract”, 
    ImportLoadPolicy.Synchronous)]
  public ContributorHelperContract ContributorHelper { get; set; }

  #endregion

图 7 ObaPlugin 导入的约定

约定类型 说明
LayerManagerContract 允许插件添加或删除地图层。
PopupContract 允许插件在用户光标 悬停或者单击实体/图钉 时显示弹出 UI。
ConfigurationContract 允许插件在运行时从配置文件动态加载配置值。
MapContract 允许插件控制地图。
PushpinFactoryContract 允许插件为其实体呈现标准图钉。
ContributorHelperContract 允许插件创建异步提供程序或根据需求加载的提供程序。 (稍后我将使用此帮助程序将“Driving directions”和“StreetSide”提供程序链接添加到弹出窗口中。)

声明了导入之后,我将覆盖虚拟 Initialize 方法:

public override void Initialize()
{
  // Obtain the application key from configuration file
  IDictionary<string, object> configs = 
    ConfigurationContract.GetDictionary(this.Token);
  string appKey = (string)configs[“ApplicationKey”];
  _searchHelper = new ObaSearchHelper(appKey);
  _searchHelper.SearchStopsByLocationCompleted += 
    OnSearchBusStopsCompleted;
  _searchHelper.SearchArrivalTimesForStopCompleted += 
    OnSearchArrivalTimesComplete;

  // Update the bus stop every time map view changes
  Map.TargetViewChangedThrottled += (s, e) => RefreshBusStops();
}

在插件实例构建并且满足所有声明的导入后,将调用且只调用一次此方法。 这是执行依赖于导入约定的所有初始化工作的绝佳位置。 如果插件具有任何导出,则您还必须确保所有导出的属性在 Initialize 返回时已完全实例化。

我首先通过导入的 ConfigurationContract 实例从配置文件获取应用程序密钥,然后将其传递到 ObaSearchHelper 构造函数。 通过将应用程序密钥放在配置文件中,我可以方便地随时进行更改而无需重新编译项目。

接下来要做的事情是从 MapContract 实例挂接事件 TargetViewChangedThrottled。 此事件在地图视图每次将要更改时引发,这种更改可以是编程方式,也可以通过用户交互。 正如名称所表明,该事件在内部限制,因此在短期运行中不会触发过多次数。 所以,如果要保持实体与地图视图同步,这是一个绝佳的事件。 在本例中,我调用了 RefreshBusStops 方法以刷新公交车站列表。 以下为 RefreshBusStops 的定义:

internal void RefreshBusStops()
{
  if (Map.ZoomLevel >= 14)
  {
    // Search within the radius of 1km, maximum 150 results
    _searchHelper.SearchStopsByLocation(
      Map.Center.Latitude, Map.Center.Longitude, radius: 1000, 150);
  }
  else
  {
    // Clear all bus stops
    ClearBusStops();
  }
}

此处,我检查了当前地图缩放级别至少为 14,然后发布了对 SearchStopsByLocation 方法的调用,这将返回在地图中心附近指定半径内的所有公交车站列表。 否则,如果缩放级别小于 14,则意味着地图缩放级别还不够接近城市级别,则将清除所有公交车站。

SearchStopsByLocation 方法完成时(异步),将引发 SearchStopsByLocationCompleted 事件,如图 8 中所示,我之前已在 Initialize 方法中订阅该事件。

图 8 SearchBusStopsCompleted 事件的处理程序

private int _searchCount;
private Dictionary<BusStop, int> _busStopCache = 
  new Dictionary<BusStop, int>();
private ObaLayer _layer;

private void OnSearchBusStopsCompleted(object sender, BusStopsEventArgs e)
{
  _searchCount++;

  // Contains new bus stops not present in the current view 
  Collection<BusStopEntity> addedEntities = 
    new Collection<BusStopEntity>();
  foreach (BusStop stop in e.BusStops)
  {
    // If this bus stop is not in the cache, it is a new one
    if (!_busStopCache.ContainsKey(stop))
    {
      addedEntities.Add(new BusStopEntity(stop, PushpinFactory));
    }

    // Marks this bus stop as being in the current search
    _busStopCache[stop] = _searchCount;
  }

  // Contains the old bus stops 
  // that should be removed from the current view
  Collection<BusStopEntity> removedEntities = 
    new Collection<BusStopEntity>();
  foreach (BusStopEntity bse in _layer.Entities)
  {
    // This bus stop belongs to the previous search, 
    // add it to removed list
    if (_busStopCache[bse.BusStop] < _searchCount)
    {
      removedEntities.Add(bse);
      _busStopCache.Remove(bse.BusStop);
    }
  }

// Tells the layer to add new in-view entities 
// and remove out-of-view ones 
      _layer.RefreshEntities(addedEntities, removedEntities);
}

请注意类型为 ObaLayer 的对象,我将在下面的部分中予以说明。 目前可以肯定地说,该对象负责管理左边面板的内容以及地图上的实体。

请注意,我使用了 Dictionary<BusStop, int> 对象来帮助我跟踪当前显示的公交车站列表。 通过此字典的帮助,每次从服务调用中获取一组全新的公交车站时,我可以快速确定尚未显示的新车站,以及由于地图视图更改现在位于视图之外的旧车站。

您可能会有疑问,为什么不清除所有当前公交车站并将新的一组公交车站一起显示。 这是出于性能考虑。 如果采用了上述方法,则必须强制重新创建所有图钉控件,即使新旧地图视图上有许多图钉处于相同位置。 更糟糕的是,在删除图钉并快速添加回来的过程中,用户会看到快速刷新。 而我采用的方法可以消除这些缺点。

在图层上显示内容和图钉

插件类表示地图应用程序,但是如果您希望显示其 UI 表示形式,则需要创建图层。 图层是一个抽象的概念,允许您将实体作为图钉(或者折线、多边形)放置在地图上,并在左侧面板中显示自定义 UI 内容。 还可以选择将任意 UI 覆盖放置在地图之上,但在我的应用程序中不需要。 大部分应用程序只有一个图层。

如果您曾经用过 Photoshop,则会发现其概念与 Photoshop 图层非常相似。 “历史记录”按钮(位于页面的左下角)显示所有当前加载图层的列表。 您可以选择显示或隐藏任何图层,或者可以将某个图层设置为活动。 图层成为活动时,其面板 UI 元素显示在左侧面板中,并且其所有实体都将置于 z 索引堆栈的前面。

在代码中,图层是抽象 Layer 类的子类。 如图 9 中所示,ObaPlugin 创建和管理 ObaLayer 类的实例,该实例派生自 Layer。

图 9 ObaLayer 类

internal class ObaLayer : Layer
{
  private ObaPlugin _parent;
  private BusStopsPanel _resultPanel;
  private ObservableCollection<BusStopEntity> _busStopEntities;

  public ObaLayer(ObaPlug-in parent) : base(parent.Token)
  {
    _parent = parent;

    // Contains the set of active bus stop entities 
    // for data binding purpose
    _busStopEntities = new ObservableCollection<BusStopEntity>();
    _resultPanel = new BusStopsPanel(parent, this);
    _resultPanel.DataContext = _busStopEntities;

    this.Title = “Bus stops”;
    this.Panel = _resultPanel;
  }
  ...
}

在 ObaLayer 的构造函数中,我为图层设置了标题,该标题显示在左侧面板的顶部。 我还将 Panel 属性设置为 BusStopsPanel 用户控件的实例,如果我的图层活动,该控件将占据整个左侧面板区域。 用户控件的 DataContext 属性设置为 ObservableCollection<Entity> 的实例。

那么,如何使图层显示? 使用图 10 中所示的 ObaPlugin 完成。

图 10 ShowResultLayer 方法向用户显示 ObaLayer 实例

public override void Activate(IDictionary<string, string> activationParameters)
{
  ShowResultLayer();
  RefreshBusStops();
}

private void ShowResultLayer()
{
  if (_layer == null)
  {
    _layer = new ObaLayer(this);
  }

  if (LayerManager.ContainsLayer(_layer))
  {
    LayerManager.BringToFront(_layer);
  }
  else
  {
    LayerManager.AddLayer(_layer);
  }
}

每次通过地图应用程序库激活地图应用程序时,将调用 override Activate 方法。 为显示图层,我引用了之前导入的 LayerManagerContract 类型。 LayerManagerContract 类定义方法以与图层一起使用。 如果图层已添加,则通过调用 BringToFront 方法将其设置为活动。 尝试两次添加相同图层会导致异常。

图 8 中,我调用了 ObaLayer.RefreshEntities 方法以更新公交车站。 图 11 显示了其定义。

图 11 ObaLayer.RefreshEntities 方法定义

public void RefreshEntities(
  ICollection<BusStopEntity> addedEntities,
  ICollection<BusStopEntity> removedEntities)
{
  foreach (BusStopEntity entity in removedEntities)
  {
    // Remove this pushpin from the map
    this.Entities.Remove(entity);
    // Remove this bus stop entry from the panel
    _busStopEntities.Remove(entity);
  }

  foreach (BusStopEntity entity in addedEntities)
  {
    // Add this pushpin to the map
    this.Entities.Add(entity);
    // Add this bus stop entry to the panel
    _busStopEntities.Add(entity);

    // Register this entity to have popup behavior
    _parent.PopupContract.Register(entity, OnPopupStateChangeHandler);
  }
}

为在地图上添加或删除实体,我使用了 Layer.Entities 集合属性。 并且,对于每个新 BusStopEntity,我调用 PopupContract.Register 方法以在公交车站图钉上注册弹出 UI。 Register 方法接受实体和回调方法,该回调方法在弹出式控件更改实体状态时调用(请参见图 12)。

图 12 更改弹出式控件的状态

private void OnPopupStateChangeHandler(PopupStateChangeContext context)
{
  if (context.State == PopupState.Closed)
  {
    return;
  }

  BusStopEntity entity = (BusStopEntity)context.Entity;
  context.Title = entity.Name;
  context.Content = “Bus numbers: “ + entity.BusRoutesAsString;

  // Only shows contributors link in the normal state of popup
  if (context.State == Pop-upState.Normal)
  {
    // Add Arrival times contributor
    context.Contributors.Add(new BusStopContributor(_parent, entity));

    Dictionary<string, object> parameter = new Dictionary<string, object>
    {
      {“Entity”, entity}
    };
    var contributorHelper = _parent.ContributorHelper;

    // Add Directions contributor
    context.Contributors.Add(contributorHelper.
CreateDemandLoadContributor(
      “Microsoft/DirectionsContributorFactoryContract”, 
      parameter, “Directions”));

    // Add Streetside contributor
    context.Contributors.Add(contributorHelper.CreateAsyncContributor(
      “Microsoft/StreetsideContributorFactoryContract”, parameter));
  }
}

在弹出式控件回调内部,PopupStateChangeContext 类型的方法参数提供了对弹出式控件的当前状态以及弹出式控件下的当前实体的访问。 根据这些内容,我使用公交车站名称设置弹出式控件标题,使用 BusStopEntity.BusRoutesAsString 属性设置弹出式控件内容,该属性将返回停靠此特定公交车站的公交线路列表,以逗号分隔。

如果弹出式控件处于 Normal 状态,则还通过 Contributors 集合属性将三个提供程序链接添加到弹出式控件,其中“Arrival times”提供程序属性显示此公交车站的到站时间,Directions 提供程序调用行车路线功能,Streetside 内容提供程序切换到街道级别的地图模式。

提供程序使用弹出式控件底部的超链接表示。 它允许地图应用程序调用 Bing Maps 的一个特定核心功能。 要生成提供程序,您可以调用 ContributorHelperContract 类型的两种方法之一:CreateAsyncContributor 或 CreateDemandLoadContributor。 这两种方法均异步返回代理提供程序,并延迟加载真实的提供程序实例。 唯一的差别是 CreateAsyncContributor 在返回时立即加载真实提供程序,而 CreateDemandLoadContributor 仅在首次调用代理提供程序链接时进行加载。

BusStopsPanel UserControl

BusStopsPanel 是一个 UserControl,负责在左侧面板内显示视图内的公交车站列表(如图 1 所示)。 其中包含将 ItemTemplate 属性设置为自定义 DataTemplate 的 ItemsControl 实例(请参见图 13)。 请注意,通过将 ItemsPanel 属性设置为使用 VirtualizingStackPanel,我为 ItemsControl 启用了 UI 虚拟化模式。 通常,如果应用程序可能将数百个项目加载到其中,则对 ItemsControl 应用 UI 虚拟化是一种好方法。

图 13 BusStopsPanel UserControl

<UserControl.Resources>
  <DataTemplate x:Key=”BusStopTemplate”>
    <Border Margin=”2,0,2,12”
      <StackPanel>
        <TextBlock Text=”{Binding Name}” FontSize=”14” FontWeight=”Bold”  
          TextWrapping=”Wrap” />
        <TextBlock Text=”{Binding BusRoutesAsString, StringFormat=’Bus numbers:
          {0}’}” FontSize=”12” TextWrapping=”Wrap” />
          <HyperlinkButton Content=”Arrival times” Click=”OnBusStopClick” 
            Style=”{StaticResource App.P2.Hyperlink}”         
              HorizontalAlignment=”Left” />
      </StackPanel>
    </Border>
  </DataTemplate>

  <ControlTemplate x:Key=”ScrollableItemsControl”    
    TargetType=”ItemsControl”>
    <ScrollViewer Style=”{StaticResource App.ScrollViewer}”  
      VerticalScrollBarVisibility=”Auto”>
      <ItemsPresenter />
    </ScrollViewer>
  </ControlTemplate>
</UserControl.Resources>
    
<ItemsControl 
  ItemsSource=”{Binding}”
  VirtualizingStackPanel.VirtualizationMode=”Recycling”
  ItemTemplate=”{StaticResource BusStopTemplate}” 
  Template=”{StaticResource ScrollableItemsControl}”>
  <ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
      <VirtualizingStackPanel />
    </ItemsPanelTemplate>
  </ItemsControl.ItemsPanel>
</ItemsControl>

在公交车站 DataTemplate 中,我添加了 HyperlinkButton 控件,单击该控件时将触发搜索相应公交车站的到站时间:

private void OnBusStopClick(object sender, RoutedEventArgs e)
{
  FrameworkElement element = (FrameworkElement)sender;
  BusStopEntity entity = (BusStopEntity)element.DataContext;
  _plugin.SearchArrivalTimes(entity);
}

请注意,其 Style 属性设置为来自 StaticResource 集合的对象,并且键设置为“App.P2.Hyperlink”。Bing Maps 提供了一组默认 UI 资源,如常用控件的样式和 ControlTemplates,以及 Bing Maps 自身使用的标准颜色和画笔。 建议地图应用程序作者对其 UI 元素应用这些资源,使其具有与本机 UI 元素相同的外观和使用感受。 有关 Bing Maps 提供的所有资源,请参阅文档。

SearchArrivalTimes 是 ObaPlugin 类的内部方法。 它调用 ObaSearchHelper.SearchArrivalTimesForStop 方法以检索指定公交车站的到达时间:

internal void SearchArrivalTimes(BusStopEntity _entity)
{
  _searchHelper.SearchArrivalTimesForStop(_entity.StopId, _entity);
}

搜索完成后,插件将指示 ObaLayer 在左侧窗格中显示到站时间。 ObaLayer 通过动态更改其 Title 和 Panel 属性来完成此操作。

地图应用程序的测试和调试

完成编码之后,您将需要测试应用程序。 Bing Maps 站点提供了开发人员模式,可通过在 URL 中附加“developer=1”查询参数来启用,如下所示:https://www.bing.com/maps/explore/?developer=1。 在开发人员模式中时,您可以使用“地图应用程序测试工具”测试地图应用程序,该工具可通过相同地图应用程序库激活。 使用地图应用程序测试工具可以从本地硬盘驱动器中选择插件程序集。 然后,它将插件加载到站点中,如同对所有本机插件一样。 要调试代码,请在加载应用程序时将 VS.NET 与浏览器关联。 请确保将调试代码类型设置为“Silverlight”。

提交您的地图应用程序

最后,在您确认对代码满意时,可以提交应用程序以正式发布到 Bing Maps 站点上。 在 Bing Maps 帐户中心 (bingmapsportal.com) 中,您可以提交新应用程序和查看以前提交应用程序的状态。 SDK 文档中提供了关于提交应用程序的详细说明,并且列出了应用程序获得批准所需满足的要求列表。

倡议

这样,您便完成了工作:成熟、实时传输应用程序,无需太多代码。 使用 SDK 提供的构建块编写以地图为中心的应用程序的体验轻松且获益颇多。 建议您立即下载 SDK、学习并开始编写自己的地图应用程序。

Luan Nguyen   在 Bing Maps 小组(以前称为 Microsoft Virtual Earth)作为开发人员工作已接近四年。 他是 Shell 功能小组的成员,该小组负责 Bing Maps 站点和 Bing Map 应用程序 SDK 的框架 Shell。 最近他调到了 ASP.NET 小组。

衷心感谢以下技术专家对本文的审阅:Alan PaulinChris PendletonDan PolivyGreg Schechter