Compartilhar via


Usar o AJAX para implementar cenários de mapeamento

pela Microsoft

Baixar PDF

Esta é a etapa 11 de um tutorial gratuito de aplicativo "NerdDinner" que explica como criar um aplicativo Web pequeno, mas completo, usando ASP.NET MVC 1.

A etapa 11 mostra como integrar o suporte ao mapeamento do AJAX em nosso aplicativo NerdDinner, permitindo que os usuários que estão criando, editando ou exibindo jantares vejam o local do jantar graficamente.

Se você estiver usando ASP.NET MVC 3, recomendamos que siga os tutoriais do Introdução With MVC 3 ou MVC Music Store.

NerdDinner Etapa 11: Integrando um mapa AJAX

Agora tornaremos nosso aplicativo um pouco mais interessante visualmente integrando o suporte ao mapeamento do AJAX. Isso permitirá que os usuários que estão criando, editando ou exibindo jantares vejam o local do jantar graficamente.

Criando uma exibição parcial do mapa

Vamos usar a funcionalidade de mapeamento em vários locais em nosso aplicativo. Para manter nosso código DRY, encapsularemos a funcionalidade de mapa comum em um único modelo parcial que podemos reutilizar em várias ações e exibições do controlador. Vamos nomear essa exibição parcial como "map.ascx" e criá-la no diretório \Views\Dinners.

Podemos criar a parcial map.ascx clicando com o botão direito do mouse no diretório \Views\Dinners e escolhendo o comando de menu Adicionar Exibição>. Vamos nomear o modo de exibição "Map.ascx", marcar-lo como uma exibição parcial e indicar que vamos passar uma classe de modelo "Dinner" fortemente tipada:

Captura de tela da caixa de diálogo Adicionar Exibição. Nerd Dinner dot Models dot Dinner é escrito na caixa Exibir classe de dados.

Quando clicarmos no botão "Adicionar", nosso modelo parcial será criado. Em seguida, atualizaremos o arquivo Map.ascx para ter o seguinte conteúdo:

<script src="http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2" type="text/javascript"></script>
<script src="/Scripts/Map.js" type="text/javascript"></script>

<div id="theMap">
</div>

<script type="text/javascript">
   
    $(document).ready(function() {
        var latitude = <%=Model.Latitude%>;
        var longitude = <%=Model.Longitude%>;
                
        if ((latitude == 0) || (longitude == 0))
            LoadMap();
        else
            LoadMap(latitude, longitude, mapLoaded);
    });
      
   function mapLoaded() {
        var title = "<%=Html.Encode(Model.Title) %>";
        var address = "<%=Html.Encode(Model.Address) %>";
    
        LoadPin(center, title, address);
        map.SetZoomLevel(14);
    } 
      
</script>

A primeira <referência de script> aponta para a biblioteca de mapeamento da Terra Virtual da Microsoft 6.2. A segunda <referência de script> aponta para um arquivo de map.js que criaremos em breve, o que encapsulará nossa lógica de mapeamento javascript comum. O <elemento div id="theMap"> é o contêiner HTML que a Terra Virtual usará para hospedar o mapa.

Em seguida, temos um bloco de script> inserido <que contém duas funções JavaScript específicas para essa exibição. A primeira função usa jQuery para conectar uma função que é executada quando a página está pronta para executar o script do lado do cliente. Ele chama uma função auxiliar LoadMap() que definiremos em nosso arquivo de script Map.js para carregar o controle de mapa da Terra virtual. A segunda função é um manipulador de eventos de retorno de chamada que adiciona um pino ao mapa que identifica um local.

Observe como estamos usando um bloco %= %> do lado <do servidor dentro do bloco de script do lado do cliente para inserir a latitude e a longitude do Dinner que desejamos mapear para o JavaScript. Essa é uma técnica útil para gerar valores dinâmicos que podem ser usados pelo script do lado do cliente (sem exigir uma chamada AJAX separada de volta para o servidor para recuperar os valores – o que o torna mais rápido). Os <blocos %= %> serão executados quando a exibição estiver sendo renderizada no servidor e, portanto, a saída do HTML acabará apenas com valores JavaScript inseridos (por exemplo: latitude var = 47,64312;).

Criando uma biblioteca do utilitário Map.js

Agora vamos criar o arquivo Map.js que podemos usar para encapsular a funcionalidade javaScript para nosso mapa (e implementar os métodos LoadMap e LoadPin acima). Podemos fazer isso clicando com o botão direito do mouse no diretório \Scripts em nosso projeto e, em seguida, escolha o comando de menu "Adicionar Novo Item", selecione o item JScript e nomeie-o> como "Map.js".

Abaixo está o código JavaScript que adicionaremos ao arquivo Map.js que interagirá com a Terra Virtual para exibir nosso mapa e adicionar pinos de localização a ele para nossos jantares:

var map = null;
var points = [];
var shapes = [];
var center = null;

function LoadMap(latitude, longitude, onMapLoaded) {
    map = new VEMap('theMap');
    options = new VEMapOptions();
    options.EnableBirdseye = false;

    // Makes the control bar less obtrusize.
    map.SetDashboardSize(VEDashboardSize.Small);
    
    if (onMapLoaded != null)
        map.onLoadMap = onMapLoaded;

    if (latitude != null && longitude != null) {
        center = new VELatLong(latitude, longitude);
    }

    map.LoadMap(center, null, null, null, null, null, null, options);
}

function LoadPin(LL, name, description) {
    var shape = new VEShape(VEShapeType.Pushpin, LL);

    //Make a nice Pushpin shape with a title and description
    shape.SetTitle("<span class=\"pinTitle\"> " + escape(name) + "</span>");
    if (description !== undefined) {
        shape.SetDescription("<p class=\"pinDetails\">" + 
        escape(description) + "</p>");
    }
    map.AddShape(shape);
    points.push(LL);
    shapes.push(shape);
}

function FindAddressOnMap(where) {
    var numberOfResults = 20;
    var setBestMapView = true;
    var showResults = true;

    map.Find("", where, null, null, null,
           numberOfResults, showResults, true, true,
           setBestMapView, callbackForLocation);
}

function callbackForLocation(layer, resultsArray, places,
            hasMore, VEErrorMessage) {
            
    clearMap();

    if (places == null) 
        return;

    //Make a pushpin for each place we find
    $.each(places, function(i, item) {
        description = "";
        if (item.Description !== undefined) {
            description = item.Description;
        }
        var LL = new VELatLong(item.LatLong.Latitude,
                        item.LatLong.Longitude);
                        
        LoadPin(LL, item.Name, description);
    });

    //Make sure all pushpins are visible
    if (points.length > 1) {
        map.SetMapView(points);
    }

    //If we've found exactly one place, that's our address.
    if (points.length === 1) {
        $("#Latitude").val(points[0].Latitude);
        $("#Longitude").val(points[0].Longitude);
    }
}

function clearMap() {
    map.Clear();
    points = [];
    shapes = [];
}

Integrando o mapa com criar e editar formulários

Agora integraremos o suporte ao Mapa com nossos cenários existentes de Criação e Edição. A boa notícia é que isso é muito fácil de fazer e não exige que alteremos nenhum código do controlador. Como nossos modos de exibição Criar e Editar compartilham uma exibição parcial comum "DinnerForm" para implementar a interface do usuário do formulário de jantar, podemos adicionar o mapa em um só lugar e fazer com que nossos cenários Criar e Editar o usem.

Tudo o que precisamos fazer é abrir a exibição parcial \Views\Dinners\DinnerForm.ascx e atualizá-la para incluir nosso novo mapa parcial. Veja abaixo como será a aparência atualizada do DinnerForm depois que o mapa for adicionado (observação: os elementos de formulário HTML são omitidos do snippet de código abaixo para fins de brevidade):

<%= Html.ValidationSummary() %>
 
<% using (Html.BeginForm()) { %>
 
    <fieldset>

        <div id="dinnerDiv">
            <p>
               [HTML Form Elements Removed for Brevity]
            </p>                 
            <p>
               <input type="submit" value="Save"/>
            </p>
        </div>
        
        <div id="mapDiv">    
            <%Html.RenderPartial("Map", Model.Dinner); %>
        </div> 
            
    </fieldset>

    <script type="text/javascript">

        $(document).ready(function() {
            $("#Address").blur(function(evt) {
                $("#Latitude").val("");
                $("#Longitude").val("");

                var address = jQuery.trim($("#Address").val());
                if (address.length < 1)
                    return;

                FindAddressOnMap(address);
            });
        });
    
    </script>

<% } %>

A parcial DinnerForm acima usa um objeto do tipo "DinnerFormViewModel" como seu tipo de modelo (porque precisa de um objeto Dinner, bem como de um SelectList para preencher a lista suspensa de países). Nosso Mapa parcial só precisa de um objeto do tipo "Dinner" como seu tipo de modelo e, portanto, quando renderizamos o mapa parcial, estamos passando apenas a subpropriedade Dinner de DinnerFormViewModel para ele:

<% Html.RenderPartial("Map", Model.Dinner); %>

A função JavaScript que adicionamos à parcial usa jQuery para anexar um evento de "desfoque" à caixa de texto HTML "Address". Você provavelmente já ouviu falar de eventos de "foco" que são acionados quando um usuário clica ou guias em uma caixa de texto. O oposto é um evento de "desfoque" que é acionado quando um usuário sai de uma caixa de texto. O manipulador de eventos acima limpa os valores de caixa de texto de latitude e longitude quando isso acontece e, em seguida, plota o novo local de endereço em nosso mapa. Um manipulador de eventos de retorno de chamada que definimos dentro do arquivo map.js atualizará as caixas de texto de longitude e latitude em nosso formulário usando valores retornados pela terra virtual com base no endereço que demos a ele.

E agora, quando executarmos nosso aplicativo novamente e clicarmos na guia "Jantar de Host", veremos um mapa padrão exibido junto com nossos elementos de formulário de jantar padrão:

Captura de tela da página Jantar do Host com um mapa padrão exibido.

Quando digitamos um endereço e, em seguida, o mapa será atualizado dinamicamente para exibir o local e nosso manipulador de eventos preencherá as caixas de texto de latitude/longitude com os valores de localização:

Captura de tela da página Jantares Nerds com um mapa exibido.

Se salvarmos o novo jantar e abri-lo novamente para edição, descobriremos que o local do mapa é exibido quando a página é carregada:

Captura de tela da página Editar no site Nerd Dinners.

Sempre que o campo de endereço for alterado, o mapa e as coordenadas de latitude/longitude serão atualizados.

Agora que o mapa exibe o local jantar, também podemos alterar os campos de formulário Latitude e Longitude de serem caixas de texto visíveis para elementos ocultos (já que o mapa os atualiza automaticamente sempre que um endereço é inserido). Para fazer isso, mudaremos do uso do auxiliar HTML Html.TextBox() para o uso do método auxiliar Html.Hidden():

<p>
    <%= Html.Hidden("Latitude", Model.Dinner.Latitude)%>
    <%= Html.Hidden("Longitude", Model.Dinner.Longitude)%>
</p>

E agora nossos formulários são um pouco mais fáceis de usar e evitam exibir a latitude/longitude bruta (enquanto ainda os armazenam com cada Jantar no banco de dados):

Captura de tela de um mapa na página Jantares Nerds.

Integrando o Mapa com a Exibição de Detalhes

Agora que temos o mapa integrado aos cenários Criar e Editar, vamos integrá-lo também ao nosso cenário de Detalhes. Tudo o que precisamos fazer é chamar <% Html.RenderPartial("map"); %> na exibição Detalhes.

Veja abaixo a aparência do código-fonte para o modo de exibição Detalhes completo (com integração de mapa):

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent"runat="server">
    <%= Html.Encode(Model.Title) %>
</asp:Content>

<asp:Content ID="details" ContentPlaceHolderID="MainContent" runat="server">

    <div id="dinnerDiv">

        <h2><%=Html.Encode(Model.Title) %></h2>
        <p>
            <strong>When:</strong> 
            <%=Model.EventDate.ToShortDateString() %> 

            <strong>@</strong>
            <%=Model.EventDate.ToShortTimeString() %>
        </p>
        <p>
            <strong>Where:</strong> 
            <%=Html.Encode(Model.Address) %>,
            <%=Html.Encode(Model.Country) %>
        </p>
         <p>
            <strong>Description:</strong> 
            <%=Html.Encode(Model.Description) %>
        </p>       
        <p>
            <strong>Organizer:</strong> 
            <%=Html.Encode(Model.HostedBy) %>
            (<%=Html.Encode(Model.ContactPhone) %>)
        </p>
    
        <%Html.RenderPartial("RSVPStatus"); %>
        <%Html.RenderPartial("EditAndDeleteLinks"); %>
 
    </div>
    
    <div id="mapDiv">
        <%Html.RenderPartial("map"); %>    
    </div>   
         
</asp:Content>

E agora, quando um usuário navega para uma URL /Dinners/Details/[id], ele verá detalhes sobre o jantar, o local do jantar no mapa (completo com um pino que, quando focalizado, exibe o título do jantar e o endereço dele) e tem um link do AJAX para RSVP para ele:

Captura de tela da página da Web Nerd Dinners. Um mapa é mostrado.

Implementando a pesquisa de localização em nosso banco de dados e repositório

Para concluir nossa implementação do AJAX, vamos adicionar um Mapa à home page do aplicativo que permite que os usuários pesquisem graficamente jantares próximos a eles.

Captura de tela da home page do Nerd Dinners. Um mapa é mostrado.

Começaremos implementando o suporte em nosso banco de dados e camada de repositório de dados para executar com eficiência uma pesquisa de raio baseada em localização para Jantares. Poderíamos usar os novos recursos geoespaciais do SQL 2008 para implementar isso ou, como alternativa, podemos usar uma abordagem de função SQL que Gary Dryden discutiu no artigo aqui: http://www.codeproject.com/KB/cs/distancebetweenlocations.aspx.

Para implementar essa técnica, abriremos o "Servidor Explorer" no Visual Studio, selecionaremos o banco de dados NerdDinner e clicaremos com o botão direito do mouse no sub nó "funções" nele e optaremos por criar uma nova "função com valor escalar":

Captura de tela do servidor Explorer no Visual Studio. O banco de dados Nerd Dinner está selecionado e o sub nó de funções está selecionado. A Função Com Valor Escalar está realçada.

Em seguida, colaremos a seguinte função DistanceBetween:

CREATE FUNCTION [dbo].[DistanceBetween](@Lat1 as real,
                @Long1 as real, @Lat2 as real, @Long2 as real)
RETURNS real
AS
BEGIN

DECLARE @dLat1InRad as float(53);
SET @dLat1InRad = @Lat1 * (PI()/180.0);
DECLARE @dLong1InRad as float(53);
SET @dLong1InRad = @Long1 * (PI()/180.0);
DECLARE @dLat2InRad as float(53);
SET @dLat2InRad = @Lat2 * (PI()/180.0);
DECLARE @dLong2InRad as float(53);
SET @dLong2InRad = @Long2 * (PI()/180.0);

DECLARE @dLongitude as float(53);
SET @dLongitude = @dLong2InRad - @dLong1InRad;
DECLARE @dLatitude as float(53);
SET @dLatitude = @dLat2InRad - @dLat1InRad;
/* Intermediate result a. */
DECLARE @a as float(53);
SET @a = SQUARE (SIN (@dLatitude / 2.0)) + COS (@dLat1InRad)
                 * COS (@dLat2InRad)
                 * SQUARE(SIN (@dLongitude / 2.0));
/* Intermediate result c (great circle distance in Radians). */
DECLARE @c as real;
SET @c = 2.0 * ATN2 (SQRT (@a), SQRT (1.0 - @a));
DECLARE @kEarthRadius as real;
/* SET kEarthRadius = 3956.0 miles */
SET @kEarthRadius = 6376.5;        /* kms */

DECLARE @dDistance as real;
SET @dDistance = @kEarthRadius * @c;
return (@dDistance);
END

Em seguida, criaremos uma nova função com valor de tabela no SQL Server que chamaremos de "NearestDinners":

Captura de tela do Servidor S Q L. Table-Valued função está realçada.

Esta função de tabela "NearestDinners" usa a função auxiliar DistanceBetween para retornar todos os Jantares dentro de 160 km da latitude e longitude que fornecemos:

CREATE FUNCTION [dbo].[NearestDinners]
      (
      @lat real,
      @long real
      )
RETURNS  TABLE
AS
      RETURN
      SELECT Dinners.DinnerID
      FROM   Dinners 
      WHERE  dbo.DistanceBetween(@lat, @long, Latitude, Longitude) <100

Para chamar essa função, primeiro abriremos o designer de LINQ to SQL clicando duas vezes no arquivo NerdDinner.dbml em nosso diretório \Models:

Captura de tela do arquivo nerd dinner dot d b m l no diretório Models.

Em seguida, arrastaremos as funções NearestDinners e DistanceBetween para o designer LINQ to SQL, o que fará com que elas sejam adicionadas como métodos em nossa classe NerdDinnerDataContext LINQ to SQL:

Captura de tela das funções Jantares e Distância Entre Mais Próximos.

Em seguida, podemos expor um método de consulta "FindByLocation" em nossa classe DinnerRepository que usa a função NearestDinner para retornar jantares futuros que estão dentro de 160 km do local especificado:

public IQueryable<Dinner> FindByLocation(float latitude, float longitude) {

   var dinners = from dinner in FindUpcomingDinners()
                 join i in db.NearestDinners(latitude, longitude)
                 on dinner.DinnerID equals i.DinnerID
                 select dinner;

   return dinners;
}

Implementando um método de ação de pesquisa AJAX baseado em JSON

Agora implementaremos um método de ação do controlador que aproveita o novo método de repositório FindByLocation() para retornar uma lista de dados dinner que podem ser usados para preencher um mapa. Esse método de ação retornará os dados do Dinner em um formato JSON (JavaScript Object Notation) para que eles possam ser facilmente manipulados usando JavaScript no cliente.

Para implementar isso, criaremos uma nova classe "SearchController" clicando com o botão direito do mouse no diretório \Controllers e escolhendo o comando de menu Adicionar Controlador>. Em seguida, implementaremos um método de ação "SearchByLocation" na nova classe SearchController, como abaixo:

public class JsonDinner {
    public int      DinnerID    { get; set; }
    public string   Title       { get; set; }
    public double   Latitude    { get; set; }
    public double   Longitude   { get; set; }
    public string   Description { get; set; }
    public int      RSVPCount   { get; set; }
}

public class SearchController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    //
    // AJAX: /Search/SearchByLocation

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult SearchByLocation(float longitude, float latitude) {

        var dinners = dinnerRepository.FindByLocation(latitude,longitude);

        var jsonDinners = from dinner in dinners
                          select new JsonDinner {
                              DinnerID = dinner.DinnerID,
                              Latitude = dinner.Latitude,
                              Longitude = dinner.Longitude,
                              Title = dinner.Title,
                              Description = dinner.Description,
                              RSVPCount = dinner.RSVPs.Count
                          };

        return Json(jsonDinners.ToList());
    }
}

O método de ação SearchByLocation do SearchController chama internamente o método FindByLocation no DinnerRepository para obter uma lista de jantares próximos. No entanto, em vez de retornar os objetos Dinner diretamente ao cliente, ele retorna objetos JsonDinner. A classe JsonDinner expõe um subconjunto de propriedades dinner (por exemplo: por motivos de segurança, ela não divulga os nomes das pessoas que têm RSVP'd para um jantar). Ele também inclui uma propriedade RSVPCount que não existe no Dinner e que é calculada dinamicamente contando o número de objetos RSVP associados a um jantar específico.

Em seguida, estamos usando o método auxiliar Json() na classe base Controller para retornar a sequência de jantares usando um formato de fio baseado em JSON. JSON é um formato de texto padrão para representar estruturas de dados simples. Veja abaixo um exemplo de como é uma lista formatada em JSON de dois objetos JsonDinner quando retornada de nosso método de ação:

[{"DinnerID":53,"Title":"Dinner with the Family","Latitude":47.64312,"Longitude":-122.130609,"Description":"Fun dinner","RSVPCount":2}, 
{"DinnerID":54,"Title":"Another Dinner","Latitude":47.632546,"Longitude":-122.21201,"Description":"Dinner with Friends","RSVPCount":3}]

Chamando o método AJAX baseado em JSON usando jQuery

Agora estamos prontos para atualizar a home page do aplicativo NerdDinner para usar o método de ação SearchByLocation do SearchController. Para fazer isso, abriremos o modelo de exibição /Views/Home/Index.aspx e o atualizaremos para ter uma caixa de texto, um botão de pesquisa, nosso mapa e um <elemento div> chamado dinnerList:

<h2>Find a Dinner</h2>

<div id="mapDivLeft">

    <div id="searchBox">
        Enter your location: <%=Html.TextBox("Location") %>
        <input id="search" type="submit" value="Search"/>
    </div>

    <div id="theMap">
    </div>

</div>

<div id="mapDivRight">
    <div id="dinnerList"></div>
</div>

Em seguida, podemos adicionar duas funções JavaScript à página:

<script type="text/javascript">

    $(document).ready(function() {
        LoadMap();
    });

    $("#search").click(function(evt) {
        var where = jQuery.trim($("#Location").val());
        if (where.length < 1) 
            return;

        FindDinnersGivenLocation(where);
    });

</script>

A primeira função JavaScript carrega o mapa quando a página é carregada pela primeira vez. A segunda função JavaScript conecta um manipulador de eventos de clique javaScript no botão de pesquisa. Quando o botão é pressionado, ele chama a função JavaScript FindDinnersGivenLocation() que adicionaremos ao nosso arquivo Map.js:

function FindDinnersGivenLocation(where) {
    map.Find("", where, null, null, null, null, null, false,
       null, null, callbackUpdateMapDinners);
}

Essa função FindDinnersGivenLocation() chama o mapa. Find() no Controle da Terra Virtual para centralizar o local inserido. Quando o serviço de mapa da Terra virtual retorna, o mapa. O método Find() invoca o método de retorno de chamada callbackUpdateMapDinners que passamos como o argumento final.

O método callbackUpdateMapDinners() é onde o trabalho real é feito. Ele usa o método auxiliar $.post() do jQuery para executar uma chamada AJAX para o método de ação SearchByLocation() do SearchController, passando-lhe a latitude e a longitude do mapa recém-centralizado. Ele define uma função embutida que será chamada quando o método auxiliar $.post() for concluído e os resultados do jantar formatado em JSON retornados do método de ação SearchByLocation() serão passados usando uma variável chamada "dinners". Em seguida, ele faz um foreach sobre cada jantar retornado, e usa a latitude e longitude do jantar e outras propriedades para adicionar um novo pino no mapa. Ele também adiciona uma entrada de jantar à lista HTML de jantares à direita do mapa. Em seguida, ele conecta um evento de foco para os pinos e a lista HTML para que os detalhes sobre o jantar sejam exibidos quando um usuário passar o mouse sobre eles:

function callbackUpdateMapDinners(layer, resultsArray, places, hasMore, VEErrorMessage) {

    $("#dinnerList").empty();
    clearMap();
    var center = map.GetCenter();

    $.post("/Search/SearchByLocation", { latitude: center.Latitude, 
                                         longitude: center.Longitude },     
    function(dinners) {
        $.each(dinners, function(i, dinner) {

            var LL = new VELatLong(dinner.Latitude, 
                                   dinner.Longitude, 0, null);

            var RsvpMessage = "";

            if (dinner.RSVPCount == 1)
                RsvpMessage = "" + dinner.RSVPCount + "RSVP";
            else
                RsvpMessage = "" + dinner.RSVPCount + "RSVPs";

            // Add Pin to Map
            LoadPin(LL, '<a href="/Dinners/Details/' + dinner.DinnerID + '">'
                        + dinner.Title + '</a>',
                        "<p>" + dinner.Description + "</p>" + RsvpMessage);

            //Add a dinner to the <ul> dinnerList on the right
            $('#dinnerList').append($('<li/>')
                            .attr("class", "dinnerItem")
                            .append($('<a/>').attr("href",
                                      "/Dinners/Details/" + dinner.DinnerID)
                            .html(dinner.Title))
                            .append(" ("+RsvpMessage+")"));
        });

        // Adjust zoom to display all the pins we just added.
        map.SetMapView(points);

        // Display the event's pin-bubble on hover.
        $(".dinnerItem").each(function(i, dinner) {
            $(dinner).hover(
                function() { map.ShowInfoBox(shapes[i]); },
                function() { map.HideInfoBox(shapes[i]); }
            );
        });
    }, "json");

E agora, quando executarmos o aplicativo e visitarmos a home page, receberemos um mapa. Quando inserirmos o nome de uma cidade, o mapa exibirá os próximos jantares próximos a ele:

Captura de tela da home page do Jantar Nerd com um mapa mostrado.

Passar o mouse sobre um jantar exibirá detalhes sobre ele.

Clicar no título do jantar na bolha ou no lado direito da lista HTML nos navegará até o jantar – que, opcionalmente, podemos RSVP para:

Captura de tela da página de detalhes do Jantar Nerd com um mapa mostrando a navegação para um jantar.

Próxima etapa

Agora implementamos toda a funcionalidade de aplicativo do nosso aplicativo NerdDinner. Agora vamos examinar como podemos habilitar o teste de unidade automatizado dele.