Usare AJAX per implementare scenari di mapping

da Microsoft

Scarica il PDF

Questo è il passaggio 11 di un'esercitazione gratuita sull'applicazione "NerdDinner" che illustra come creare un'applicazione Web di piccole dimensioni, ma completa, usando ASP.NET MVC 1.

Il passaggio 11 illustra come integrare il supporto del mapping AJAX nell'applicazione NerdDinner, consentendo agli utenti che stanno creando, modificando o visualizzando le cene per visualizzare la posizione della cena graficamente.

Se si usa ASP.NET MVC 3, è consigliabile seguire le esercitazioni di Introduzione With MVC 3 o MVC Music Store.

Passaggio 11 nerdDinner: Integrazione di una mappa AJAX

L'applicazione sarà ora un po' più interessante a livello visivo integrando il supporto del mapping AJAX. In questo modo gli utenti che creano, modificano o visualizzano le cene per visualizzare la posizione della cena graficamente.

Creazione di una visualizzazione parziale mappa

Verrà usata la funzionalità di mapping in diverse posizioni all'interno dell'applicazione. Per mantenere il codice DRY, verrà incapsulata la funzionalità comune della mappa all'interno di un singolo modello parziale che è possibile riutilizzare tra più azioni e visualizzazioni del controller. Verrà assegnare un nome a questa visualizzazione parziale "map.ascx" e crearlo all'interno della directory \Views\Dinners.

È possibile creare la mappa.ascx parziale facendo clic con il pulsante destro del mouse sulla directory \Views\Dinners e scegliendo il comando di menu Aggiungi visualizzazione>. Verrà assegnare un nome alla visualizzazione "Map.ascx", controllarlo come visualizzazione parziale e indicare che passeremo una classe di modello "Dinner" fortemente tipizzata:

Screenshot della finestra di dialogo Aggiungi visualizzazione. La cena a puntini di cena nerd è scritta nella casella Della classe di dati Visualizza.

Quando si fa clic sul pulsante "Aggiungi" verrà creato il modello parziale. Verrà quindi aggiornato il file Map.ascx per avere il contenuto seguente:

<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>

Il primo <riferimento dello script> punta alla libreria di mapping Microsoft Virtual Earth 6.2. Il secondo <riferimento dello script> punta a un file map.js che verrà creato brevemente che incapsulerà la logica di mapping Javascript comune. L'elemento <div id="theMap"> è il contenitore HTML che La Terra virtuale userà per ospitare la mappa.

È quindi disponibile un blocco di script> incorporato <che contiene due funzioni JavaScript specifiche per questa visualizzazione. La prima funzione usa jQuery per collegare una funzione che viene eseguita quando la pagina è pronta per eseguire lo script lato client. Chiama una funzione helper LoadMap() che verrà definita all'interno del file di script Map.js per caricare il controllo mappa della terra virtuale. La seconda funzione è un gestore eventi di callback che aggiunge un pin alla mappa che identifica una posizione.

Si noti come si usa un blocco %= %>= lato server all'interno del blocco script lato <client per incorporare la latitudine e la longitudine della cena che si vuole eseguire il mapping in JavaScript. Questa è una tecnica utile per generare valori dinamici che possono essere usati dallo script lato client (senza richiedere una chiamata AJAX separata al server per recuperare i valori, che rende più veloce). I <blocchi %= %> verranno eseguiti quando la visualizzazione viene eseguita nel server e quindi l'output del codice HTML finirà con valori JavaScript incorporati, ad esempio var latitudine = 47.64312;.

Creazione di una libreria di utilità Map.js

Verrà ora creato il file Map.js che è possibile usare per incapsulare la funzionalità JavaScript per la mappa e implementare i metodi LoadMap e LoadPin precedenti. È possibile eseguire questa operazione facendo clic con il pulsante destro del mouse sulla directory \Script all'interno del progetto e quindi scegliere il comando di menu "Aggiungi-Nuovo> elemento", selezionare l'elemento JScript e denominarlo "Map.js".

Di seguito è riportato il codice JavaScript che verrà aggiunto al file Map.js che interagisce con La Terra virtuale per visualizzare la mappa e aggiungere le posizioni a esso per le nostre cene:

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 = [];
}

Integrazione della mappa con Create and Edit Forms

Verrà ora integrato il supporto mappa con gli scenari Di creazione e modifica esistenti. La buona notizia è che questo è abbastanza facile da fare e non richiede di modificare alcun codice del controller. Poiché le visualizzazioni Create and Edit condividono una visualizzazione parziale "DinnerForm" comune per implementare l'interfaccia utente del modulo di cena, è possibile aggiungere la mappa in un'unica posizione e disporre sia dei nostri scenari di creazione che di modifica.

Tutto ciò che dobbiamo fare è aprire la visualizzazione parziale \Views\Dinners\DinnerForm.ascx e aggiornarla per includere la nuova mappa parziale. Di seguito è riportato l'aspetto di DinnerForm aggiornato dopo l'aggiunta della mappa (nota: gli elementi del modulo HTML vengono omessi dal frammento di codice seguente per brevità):

<%= 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>

<% } %>

L'oggetto DinnerForm parziale precedente accetta un oggetto di tipo "DinnerFormViewModel" come tipo di modello (perché necessita sia di un oggetto Dinner che di un oggetto SelectList per popolare l'elenco a discesa dei paesi). La mappa parziale richiede solo un oggetto di tipo "Dinner" come tipo di modello e quindi quando si esegue il rendering della mappa parziale si passa solo la sotto proprietà Dinner di DinnerFormViewModel a esso:

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

La funzione JavaScript aggiunta alla casella di testo HTML "Address" usa jQuery per collegare un evento "blur" alla casella di testo HTML "Address". Probabilmente si è sentito parlare di eventi "focus" che vengono generati quando un utente fa clic o schede in una casella di testo. L'opposto è un evento "blur" che viene generato quando un utente chiude una casella di testo. Il gestore eventi precedente cancella i valori della casella di testo latitudine e longitudine quando si verifica e quindi traccia la nuova posizione dell'indirizzo nella mappa. Un gestore eventi di callback definito all'interno del file map.js aggiornerà quindi le caselle di testo longitudine e latitudine nel modulo usando i valori restituiti dalla terra virtuale in base all'indirizzo fornito.

E ora quando si esegue di nuovo l'applicazione e si fa clic sulla scheda "Cena host" verrà visualizzata una mappa predefinita insieme agli elementi del modulo Cena standard:

Screenshot della pagina Cena host con una mappa predefinita visualizzata.

Quando si digita un indirizzo e quindi viene visualizzata la scheda, la mappa verrà aggiornata dinamicamente per visualizzare la posizione e il gestore eventi popola le caselle di testo latitudine/longitudine con i valori di posizione:

Screenshot della pagina Cena nerd con una mappa visualizzata.

Se si salva la nuova cena e quindi si apre di nuovo per la modifica, si troverà che la posizione della mappa viene visualizzata quando la pagina viene caricata:

Screenshot della pagina Modifica nel sito Nerd Dinners.

Ogni volta che il campo dell'indirizzo viene modificato, la mappa e le coordinate di latitudine/longitudine verranno aggiornate.

Ora che la mappa visualizza la posizione della cena, è anche possibile modificare i campi modulo Latitudine e Longitudine da caselle di testo visibili invece essere elementi nascosti (poiché la mappa viene aggiornata automaticamente ogni volta che viene immesso un indirizzo). A tale scopo, si passerà dall'uso dell'helper HTML Html.TextBox() all'uso del metodo helper Html.Hidden():

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

E ora i moduli sono un po 'più intuitivi ed evitare di visualizzare la latitudine/longitudine non elaborata (mentre li archiviano ancora con ogni cena nel database):

Screenshot di una mappa nella pagina Cena nerd.

Integrazione della mappa con la visualizzazione dettagli

Ora che la mappa è integrata con i nostri scenari di creazione e modifica, è possibile integrarla anche con lo scenario Dettagli. Tutto ciò che dobbiamo fare è chiamare <% Html.RenderPartial("map"); %> all'interno della visualizzazione Dettagli.

Di seguito è riportato il codice sorgente della visualizzazione Dettagli completa (con integrazione della mappa) simile al seguente:

<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 ora quando un utente passa a un URL /Dinners/Details/[id] visualizzeranno i dettagli sulla cena, la posizione della cena sulla mappa (completa con un push-pin che quando passa il puntatore del mouse su visualizza il titolo della cena e l'indirizzo di esso) e avranno un collegamento AJAX a RSVP per esso:

Screenshot della pagina Web Nerd Dinners. Viene visualizzata una mappa.

Implementazione della ricerca del percorso nel database e nel repository

Per completare l'implementazione di AJAX, aggiungere una mappa alla home page dell'applicazione che consente agli utenti di cercare graficamente le cene vicino.

Screenshot della home page delle cene nerd. Viene visualizzata una mappa.

Si inizierà implementando il supporto all'interno del livello del database e del repository di dati per eseguire in modo efficiente una ricerca raggio basata sulla posizione per le cene. È possibile usare le nuove funzionalità geospaziali di SQL 2008 per implementare questa funzionalità oppure in alternativa è possibile usare un approccio di funzione SQL illustrato in questo articolo: http://www.codeproject.com/KB/cs/distancebetweenlocations.aspx.

Per implementare questa tecnica, si aprirà "Esplora server" in Visual Studio, selezionare il database NerdDinner e quindi fare clic con il pulsante destro del mouse sul sotto-nodo "funzioni" e scegliere di creare una nuova funzione "Scalar-valued":

Screenshot di Esplora server in Visual Studio. Il database nerd Dinner è selezionato e viene selezionato il nodo secondario delle funzioni. La funzione con valori scalari è evidenziata.

Verrà quindi incollato nella funzione DistanceBetween seguente:

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

Verrà quindi creata una nuova funzione con valori di tabella in SQL Server che verrà chiamata "NearestDinners":

Screenshot del server S Q L. Table-Valued funzione è evidenziata.

Questa funzione di tabella "NearestDinners" usa la funzione helper DistanceBetween per restituire tutte le cene entro 100 miglia dalla latitudine e la longitudine fornite:

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

Per chiamare questa funzione, verrà prima aperta la finestra di progettazione LINQ to SQL facendo doppio clic sul file NerdDinner.dbml all'interno della directory \Models:

Screenshot del file Nerd Dinner dot d b m l nella directory Modelli.

Verranno quindi trascinate le funzioni NearestDinners e DistanceBetween nella finestra di progettazione LINQ to SQL, che le causeranno l'aggiunta come metodi nella classe LINQ to SQL NerdDinnerDataContext:

Screenshot delle funzioni Cena più vicina e Distanza tra le funzioni.

È quindi possibile esporre un metodo di query "FindByLocation" nella classe DinnerRepository che usa la funzione NearestDinner per restituire le prossime cene entro 100 miglia dalla posizione specificata:

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;
}

Implementazione di un metodo di azione di ricerca AJAX basato su JSON

A questo punto verrà implementato un metodo di azione controller che sfrutta il nuovo metodo repository FindByLocation() per restituire un elenco di dati Dinner che possono essere usati per popolare una mappa. Questo metodo di azione restituirà i dati Dinner in un formato JSON (JavaScript Object Notation) in modo che possa essere facilmente manipolato usando JavaScript nel client.

Per implementare questa operazione, verrà creata una nuova classe "SearchController" facendo clic con il pulsante destro del mouse sulla directory \Controller e scegliendo il comando di menu Add-Controller>. Verrà quindi implementato un metodo di azione "SearchByLocation" all'interno della nuova classe SearchController, come illustrato di seguito:

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());
    }
}

Il metodo di azione SearchByLocation di SearchController chiama internamente il metodo FindByLocation in DinnerRepository per ottenere un elenco di cene vicine. Anziché restituire gli oggetti Dinner direttamente al client, tuttavia, restituisce oggetti JsonDinner. La classe JsonDinner espone un subset di proprietà Dinner (ad esempio: per motivi di sicurezza non rivela i nomi delle persone che hanno RSVP'd per una cena). Include anche una proprietà RSVPCount che non esiste su Dinner e che viene calcolata in modo dinamico conteggiando il numero di oggetti RSVP associati a una determinata cena.

Si usa quindi il metodo helper Json() nella classe base Controller per restituire la sequenza di cene usando un formato di filo basato su JSON. JSON è un formato di testo standard per rappresentare semplici strutture dati. Di seguito è riportato un esempio di come viene visualizzato un elenco formattato JSON di due oggetti JsonDinner quando viene restituito dal metodo di azione:

[{"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}]

Chiamata al metodo AJAX basato su JSON con jQuery

È ora possibile aggiornare la home page dell'applicazione NerdDinner per usare il metodo di azione SearchByLocation di SearchController. A tale scopo, si aprirà il modello di visualizzazione /Views/Home/Index.aspx e lo aggiorneremo per avere una casella di testo, un pulsante di ricerca, la mappa e un <elemento div> denominato 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>

È quindi possibile aggiungere due funzioni JavaScript alla pagina:

<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>

La prima funzione JavaScript carica la mappa quando la pagina viene caricata per la prima volta. La seconda funzione JavaScript esegue il collegamento di un gestore eventi JavaScript nel pulsante di ricerca. Quando il pulsante viene premuto chiama la funzione JavaScript FindDinnersGivenLocation() che verrà aggiunta al file Map.js:

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

Mappa delle chiamate di funzione FindDinnersGivenLocation(). Find() sul controllo Della Terra virtuale per centrarlo sulla posizione immessa. Quando il servizio mappa della terra virtuale restituisce, la mappa. Il metodo Find() richiama il metodo callbackUpdateMapDinners passato come argomento finale.

Il metodo callbackUpdateMapDinners() è la posizione in cui viene eseguito il lavoro reale. Usa il metodo helper $.post() di jQuery per eseguire una chiamata AJAX al metodo di azione SearchController() di SearchController, passandola la latitudine e la longitudine della mappa appena centrale. Definisce una funzione inline che verrà chiamata quando viene completato il metodo helper $.post() e i risultati della cena in formato JSON restituiti dal metodo di azione SearchByLocation() verranno passati usando una variabile denominata "dinners". Quindi fa un foreach per ogni cena restituita e usa la latitudine e longitudine della cena e altre proprietà per aggiungere un nuovo pin sulla mappa. Aggiunge anche una voce di cena all'elenco HTML delle cene a destra della mappa. Si collega quindi un evento al passaggio del mouse per le pinne e l'elenco HTML in modo che i dettagli sulla cena vengano visualizzati quando un utente passa il puntatore del mouse su di loro:

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 ora quando si esegue l'applicazione e si visita la home page verrà presentata una mappa. Quando immetti il nome di una città, la mappa visualizzerà le prossime cene vicino a esso:

Screenshot della home page Nerd Dinner con una mappa visualizzata.

Passare il puntatore del mouse su una cena visualizzerà i dettagli su di esso.

Facendo clic sul titolo Cena nella bolla o sul lato destro nell'elenco HTML, verrà visualizzata la cena, che sarà quindi possibile scegliere RSVP per:

Screenshot della pagina Dettagli cena nerd con una mappa che mostra lo spostamento a una cena.

passaggio successivo

Ora sono state implementate tutte le funzionalità dell'applicazione dell'applicazione NerdDinner. Ora esaminiamo come è possibile abilitare unit test automatizzati di esso.