Použití jazyka AJAX k implementaci scénářů mapování
od Microsoftu
Toto je krok 11 bezplatného kurzu aplikace NerdDinner , který vás provede sestavením malé, ale kompletní webové aplikace pomocí ASP.NET MVC 1.
Krok 11 ukazuje, jak integrovat podporu mapování AJAX do naší aplikace NerdDinner a umožnit tak uživatelům, kteří vytvářejí, upravují nebo prohlížejí večeře, grafické zobrazení umístění večeře.
Pokud používáte ASP.NET MVC 3, doporučujeme postupovat podle kurzů Začínáme S MVC 3 nebo MVC Music Store.
NerdDinner Krok 11: Integrace mapy AJAX
Díky integraci podpory mapování AJAX teď naše aplikace bude vizuálně zajímavější. To umožní uživatelům, kteří vytvářejí, upravují nebo prohlížejí večeře, zobrazit umístění večeře graficky.
Vytvoření částečného zobrazení mapy
Funkci mapování budeme používat na několika místech v rámci naší aplikace. Aby byl náš kód suchý, zapouzdřeme společné funkce mapování do jedné částečné šablony, kterou můžeme znovu použít pro více akcí a zobrazení kontroleru. Toto částečné zobrazení pojmenujeme map.ascx a vytvoříme ho v adresáři \Views\Dinners.
Soubor map.ascx můžeme vytvořit částečně tak, že klikneme pravým tlačítkem na adresář \Views\Dinners a zvolíme příkaz nabídky Add-View>. Toto zobrazení pojmenujeme "Map.ascx", zkontrolujeme ho jako částečné zobrazení a označíme ho jako třídu modelu "Dinner" se silnými typy:
Když klikneme na tlačítko Přidat, vytvoří se naše částečná šablona. Potom soubor Map.ascx aktualizujeme tak, aby měl následující obsah:
<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>
První <odkaz na skript> odkazuje na knihovnu mapování Microsoft Virtual Earth 6.2. Druhý <odkaz na skript> odkazuje na soubor map.js, který zanedlouho vytvoříme a který zapouzdří naši běžnou logiku mapování JavaScriptu. Element <div id="theMap"> je kontejner HTML, který Virtual Earth použije k hostování mapy.
Pak máme blok vloženého <skriptu> , který obsahuje dvě funkce JavaScriptu specifické pro toto zobrazení. První funkce používá jQuery ke připojení funkce, která se spustí, když je stránka připravená ke spuštění skriptu na straně klienta. Volá pomocnou funkci LoadMap(), kterou definujeme v souboru skriptu Map.js pro načtení ovládacího prvku virtuální mapy země. Druhá funkce je obslužná rutina události zpětného volání, která do mapy přidá špendlík, který identifikuje umístění.
Všimněte si, jak používáme blok %= %> na straně <serveru v bloku skriptu na straně klienta k vložení zeměpisné šířky a délky večeře, kterou chceme namapovat do JavaScriptu. Jedná se o užitečnou techniku výstupu dynamických hodnot, které mohou být použity skriptem na straně klienta (bez nutnosti samostatného volání AJAX zpět na server k načtení hodnot – což je rychlejší). Bloky <%= %> se spustí, když se zobrazení vykresluje na serveru, takže výstup HTML bude jenom s vloženými javascriptovými hodnotami (například var latitude = 47.64312;).
Vytvoření knihovny nástrojů Map.js
Pojďme teď vytvořit Map.js soubor, který můžeme použít k zapouzdření funkcí JavaScriptu pro naši mapu (a implementovat výše uvedené metody LoadMap a LoadPin). Můžeme to udělat tak, že klikneme pravým tlačítkem na adresář \Scripts v rámci našeho projektu a pak zvolíme příkaz nabídky Přidat> novou položku, vybereme položku jazyka JScript a pojmenujeme ji "Map.js".
Níže je kód JavaScriptu, který přidáme do souboru Map.js, který bude pracovat s Virtual Earth, aby zobrazil naši mapu a přidal do ní připnuté polohy pro naše večeře:
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 = [];
}
Integrace mapy s vytvářením a úpravami formulářů
Teď integrujeme podporu map s našimi stávajícími scénáři vytváření a úprav. Dobrou zprávou je, že je to docela snadné a nevyžaduje, abychom změnili kód kontroleru. Vzhledem k tomu, že naše zobrazení Vytvořit a upravit sdílí společné částečné zobrazení "DinnerForm" pro implementaci uživatelského rozhraní formuláře večeře, můžeme mapu přidat na jednom místě a použít ji náš scénář Vytvořit a Upravit.
Stačí otevřít částečné zobrazení \Views\Dinners\DinnerForm.ascx a aktualizovat ho tak, aby zahrnovalo naši novou mapu částečně. Níže je uvedeno, jak bude aktualizovaný formulář DinnerForm vypadat po přidání mapy (poznámka: Prvky formuláře HTML jsou z fragmentu kódu níže vynechány pro stručnost):
<%= 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>
<% } %>
Část DinnerForm výše přebírá objekt typu "DinnerFormViewModel" jako typ modelu (protože k naplnění rozevíracího seznamu zemí potřebuje objekt Dinner i SelectList). Část mapy potřebuje jako typ modelu objekt typu "Dinner", takže když mapu vykreslíme jako částečnou, předáváme do něj jenom dílčí vlastnost DinnerFormViewModel:
<% Html.RenderPartial("Map", Model.Dinner); %>
Funkce JavaScriptu, kterou jsme přidali do částečné části, používá jQuery k připojení události rozostření k textovému poli HTML "Address" (Adresa). Pravděpodobně jste už slyšeli o událostech fokusu, které se aktivují, když uživatel klikne na textové pole nebo se do textového pole zasadí. Opakem je událost rozostření, která se aktivuje, když uživatel opustí textové pole. Výše uvedená obslužná rutina události v takovém případě vymaže hodnoty textového pole zeměpisná šířka a délka a potom vykreslí nové umístění adresy na naší mapě. Obslužná rutina události zpětného volání, kterou jsme definovali v souboru map.js, pak aktualizuje textová pole zeměpisné délky a šířky v našem formuláři pomocí hodnot vrácených virtuální zemí na základě zadané adresy.
A teď, když aplikaci znovu spustíme a klikneme na kartu Host Dinner (Host Dinner), zobrazí se výchozí mapa spolu s našimi standardními prvky formuláře Večeře:
Když napíšeme adresu a pak se tabulátorem přesuneme, mapa se dynamicky aktualizuje tak, aby zobrazovala polohu, a naše obslužná rutina události naplní textová pole zeměpisné šířky a délky hodnotami umístění:
Pokud uložíme novou večeři a znovu ji otevřeme pro úpravy, zjistíme, že se při načtení stránky zobrazí umístění mapy:
Při každé změně pole adresy se aktualizuje mapa a souřadnice zeměpisné šířky a délky.
Teď, když se na mapě zobrazuje místo večeře, můžeme také změnit pole formuláře Zeměpisná šířka a Zeměpisná délka z viditelných textových polí na skryté prvky (protože mapa je automaticky aktualizuje při každém zadání adresy). Abychom to mohli udělat, přepneme z použití pomocné rutiny HTML Html.TextBox() na metodu pomocníka Html.Hidden():
<p>
<%= Html.Hidden("Latitude", Model.Dinner.Latitude)%>
<%= Html.Hidden("Longitude", Model.Dinner.Longitude)%>
</p>
A teď jsou naše formuláře uživatelsky přívětivější a vyhněte se zobrazení nezpracované zeměpisné šířky a délky (a přitom je stále ukládáme při každé večeři v databázi):
Integrace mapy se zobrazením podrobností
Teď, když máme mapu integrovanou se scénáři Vytvořit a Upravit, můžeme ji integrovat také se scénářem Podrobností. Stačí volat <% Html.RenderPartial("map"); %> v zobrazení Podrobnosti.
Níže je vidět, jak vypadá zdrojový kód kompletního zobrazení podrobností (s integrací map):
<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>
A když teď uživatel přejde na adresu URL /Dinners/Details/[id], zobrazí se mu podrobnosti o večeři, umístění večeře na mapě (doplněné o připínáčku, který při najetí myší zobrazí název večeře a její adresu) a bude mít odkaz AJAX na RSVP:
Implementace hledání umístění v databázi a úložišti
Na závěr implementace AJAX přidáme na domovskou stránku aplikace Mapu, která uživatelům umožní graficky vyhledávat večeře v jejich blízkosti.
Začneme implementací podpory v rámci naší databázové vrstvy a vrstvy úložiště dat, abychom mohli efektivně vyhledávat funkce Dinners na základě umístění. Mohli bychom použít nové geoprostorové funkce SQL 2008 k implementaci, nebo alternativně můžeme použít přístup k funkci SQL, který Gary Dryden probral v tomto článku: http://www.codeproject.com/KB/cs/distancebetweenlocations.aspx.
Abychom mohli tuto techniku implementovat, otevřeme v sadě Visual Studio Průzkumník serveru, vybereme databázi NerdDinner, klikneme pravým tlačítkem na dílčí uzel functions pod ní a zvolíme vytvoření nové skalární funkce:
Potom vložíme následující funkci 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
Potom vytvoříme novou funkci s hodnotou tabulky v SQL Server, kterou budeme nazývat "NearestDinners":
Tato funkce tabulky "NearestDinners" používá pomocnou funkci DistanceBetween k vrácení všech večeří do 100 mil od zadané zeměpisné šířky a délky:
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
Tuto funkci zavoláme tak, že poklikáním na soubor NerdDinner.dbml v adresáři \Models nejprve otevřeme návrháře LINQ to SQL:
Potom přetáhneme funkce NearestDinners a DistanceBetween do návrháře LINQ to SQL, což způsobí jejich přidání jako metody do naší třídy LINQ to SQL NerdDinnerDataContext:
Pak můžeme zveřejnit metodu dotazu "FindByLocation" pro naši třídu DinnerRepository, která používá funkci NearestDinner k vrácení nadcházejících dinner, které jsou vzdálené do 100 mil od zadaného umístění:
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;
}
Implementace metody akcí vyhledávání AJAX založené na formátu JSON
Teď implementujeme metodu akce kontroleru, která využívá novou metodu úložiště FindByLocation() k vrácení seznamu dat Večeře, která se dají použít k naplnění mapy. Tato metoda akce vrátí data Dinner zpět ve formátu JSON (JavaScript Object Notation), aby s nimi bylo možné snadno manipulovat pomocí JavaScriptu na klientovi.
Abychom to mohli implementovat, vytvoříme novou třídu SearchController tak, že klikneme pravým tlačítkem na adresář \Controllers a zvolíme příkaz nabídky Add-Controller>. Pak implementujeme metodu akce SearchByLocation v rámci nové třídy SearchController, jak je znázorněno níže:
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());
}
}
Metoda akce SearchByLocation v SearchController interně volá metodu FindByLocation na DinnerRepository k získání seznamu blízkých večeří. Místo toho, aby se objekty Dinner vracely přímo klientovi, ale vrací objekty JsonDinner. Třída JsonDinner zveřejňuje podmnožinu vlastností Dinner (například: z bezpečnostních důvodů nezveřejňuje jména lidí, kteří mají rsvp'd na večeři). Zahrnuje také vlastnost RSVPCount, která na večeři neexistuje a která se dynamicky počítá počítáním počtu objektů RSVP přidružených ke konkrétní večeři.
Potom použijeme pomocnou metodu Json() v základní třídě Controller, abychom vrátili sekvenci večeří ve formátu drátu založeném na formátu JSON. JSON je standardní textový formát pro reprezentaci jednoduchých datových struktur. Níže je příklad toho, jak vypadá seznam dvou objektů JsonDinner ve formátu JSON, když se vrátí z naší metody action:
[{"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}]
Volání metody AJAX založené na formátu JSON pomocí jQuery
Nyní jsme připraveni aktualizovat domovskou stránku aplikace NerdDinner tak, aby používala metodu akce SearchController SearchByLocation. K tomu otevřeme šablonu zobrazení /Views/Home/Index.aspx a aktualizujeme ji tak, aby měla textové pole, tlačítko hledání, mapu a <element div> s názvem 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>
Na stránku pak můžeme přidat dvě funkce JavaScriptu:
<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>
První javascriptová funkce načte mapu při prvním načtení stránky. Druhá funkce JavaScriptu propoštá obslužnou rutinu události kliknutí v JavaScriptu na tlačítko hledání. Při stisknutí tlačítka se zavolá javascriptová funkce FindDinnersGivenLocation(), kterou přidáme do souboru Map.js:
function FindDinnersGivenLocation(where) {
map.Find("", where, null, null, null, null, null, false,
null, null, callbackUpdateMapDinners);
}
Tato funkce FindDinnersGivenLocation() volá mapu. Na virtuálním ovládacím prvku Země najděte() a zacentrujte ho na zadané místo. Když se virtuální služba mapy země vrátí, mapa. Metoda Find() vyvolá metodu zpětného volání callbackUpdateMapDinners, které jsme jí předali jako poslední argument.
Metoda callbackUpdateMapDinners() je místo, kde se provádí skutečná práce. Používá pomocnou metodu jQuery $.post() k provedení volání AJAX do metody akce SearchByLocation() naší služby SearchController – předává jí zeměpisnou šířku a délku nově vycentrované mapy. Definuje vloženou funkci, která bude volána po dokončení pomocné metody $.post() a výsledky večeře ve formátu JSON vrácené z metody akce SearchByLocation() se předají pomocí proměnné s názvem "dinners". Potom provede foreach pro každou vrácenou večeři a použije zeměpisnou šířku a délku večeře a další vlastnosti k přidání nového špendlíku na mapu. Přidá také položku večeře do html seznamu večeří napravo od mapy. Potom propojí událost přechodu myší pro připínáky i seznam HTML, aby se zobrazily podrobnosti o večeři, když na ně uživatel najede myší:
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");
A když teď aplikaci spustíme a navštívíme domovskou stránku, zobrazí se nám mapa. Když zadáme název města, mapa zobrazí nadcházející večeře v jeho blízkosti:
Když najedete myší na večeři, zobrazí se o ní podrobnosti.
Kliknutím na název Večeře v bublině nebo na pravé straně v seznamu HTML nás přejdete na večeři , kterou pak můžeme volitelně použít pro:
Další krok
Teď jsme implementovali všechny funkce aplikace NerdDinner. Pojďme se teď podívat, jak můžeme povolit jeho automatizované testování jednotek.