使用 AJAX 实现映射方案
这是免费的 “NerdDinner”应用程序教程 的第 11 步,介绍如何使用 ASP.NET MVC 1 生成小型但完整的 Web 应用程序。
步骤 11 演示如何将 AJAX 映射支持集成到我们的 NerdDinner 应用程序中,使正在创建、编辑或查看晚宴的用户能够以图形方式查看晚宴的位置。
如果你使用的是 ASP.NET MVC 3,我们建议你遵循入门与 MVC 3 或 MVC 音乐应用商店教程。
NerdDinner 步骤 11:集成 AJAX 映射
现在,我们将通过集成 AJAX 映射支持,使应用程序在视觉上更令人兴奋。 这将使正在创建、编辑或查看晚餐的用户能够以图形方式查看晚餐的位置。
创建地图分部视图
我们将在应用程序中的多个位置使用映射功能。 为了保持代码 DRY,我们将通用映射功能封装在单个部分模板中,我们可以跨多个控制器操作和视图重复使用该模板。 我们将此分部视图命名为“map.ascx”,并在 \Views\Dinners 目录中创建它。
可以通过右键单击 \Views\Dinners 目录并选择“添加>视图”菜单命令来创建 map.ascx 部分。 我们将视图命名为“Map.ascx”,将其检查为分部视图,并指示我们将向其传递强类型“Dinner”模型类:
单击“添加”按钮时,将创建部分模板。 然后,我们将更新 Map.ascx 文件,以包含以下内容:
<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>
第一个 <脚本> 引用指向 Microsoft Virtual Earth 6.2 映射库。 第二 <个脚本> 引用指向一个map.js文件,我们将很快创建该文件,该文件将封装常见的 Javascript 映射逻辑。 <div id=“theMap”>元素是 Virtual Earth 将用于托管地图的 HTML 容器。
然后,我们有了一个嵌入 <的脚本> 块,其中包含特定于此视图的两个 JavaScript 函数。 第一个函数使用 jQuery 连接在页面准备好运行客户端脚本时执行的函数。 它调用 LoadMap () 帮助程序函数,我们将在Map.js脚本文件中定义该函数来加载虚拟地球地图控件。 第二个函数是回调事件处理程序,它向地图添加一个用于标识位置的图钉。
请注意,我们如何在客户端脚本块中使用服务器端 <%= %> 块来嵌入要映射到 JavaScript 的 Dinner 的纬度和经度。 这是一种有用的技术,可用于输出动态值,客户端脚本 (可以使用这些值,而无需对服务器进行单独的 AJAX 调用来检索值,从而更快地) 。 <当视图在服务器上呈现时,将执行 %= %> 块,因此 HTML 的输出最终将仅以嵌入的 JavaScript 值 (,例如:var latitude = 47.64312;) 。
创建Map.js实用工具库
现在,让我们创建Map.js文件,该文件可用于封装地图 (的 JavaScript 功能,并实现上述) 的 LoadMap 和 LoadPin 方法。 为此,可以右键单击项目中的 \Scripts 目录,然后选择“添加新>项”菜单命令,选择 JScript 项,并将其命名为“Map.js”。
下面是我们将添加到 Map.js 文件中的 JavaScript 代码,该文件将与 Virtual Earth 交互,以显示我们的地图,并为晚餐添加位置图钉:
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 = [];
}
将地图与创建和编辑窗体集成
现在,我们将将地图支持与现有的创建和编辑方案集成。 好消息是,这很容易做到,并且不需要我们更改任何控制器代码。 由于“创建”和“编辑”视图共享一个通用的“DinnerForm”部分视图来实现晚餐窗体 UI,因此我们可以在一个位置添加地图,并让创建和编辑方案都使用它。
只需打开 \Views\Dinners\DinnerForm.ascx 分部视图并将其更新为包含新地图部分。 下面是添加地图后更新的 DinnerForm 的外观 (注意:为简洁起见,下面的代码片段中省略了 HTML 窗体元素) :
<%= 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>
<% } %>
上述部分 DinnerForm 将类型为“DinnerFormViewModel”的对象作为其模型类型 (,因为它需要 Dinner 对象以及 SelectList 来填充) 国家/地区的下拉列表。 我们的地图部分只需要一个类型为“Dinner”的对象作为其模型类型,因此当我们呈现地图部分时,我们将仅将 DinnerFormViewModel 的 Dinner 子属性传递给它:
<% Html.RenderPartial("Map", Model.Dinner); %>
我们添加到 部分的 JavaScript 函数使用 jQuery 将“模糊”事件附加到“地址”HTML 文本框。 你可能听说过当用户单击或选项卡进入文本框时触发的“焦点”事件。 相反,当用户退出文本框时触发的“模糊”事件。 发生此情况时,上述事件处理程序将清除纬度和经度文本框值,然后在地图上绘制新的地址位置。 然后,我们在 map.js 文件中定义的回调事件处理程序将使用虚拟地球基于我们提供的地址返回的值更新表单上的经度和纬度文本框。
现在,当我们再次运行应用程序并单击“Host Dinner”选项卡时,我们将看到默认地图与标准 Dinner 窗体元素一起显示:
当我们键入地址,然后按 Tab 离开时,地图将动态更新以显示位置,我们的事件处理程序将使用位置值填充纬度/经度文本框:
如果我们保存新晚餐,然后再次打开它进行编辑,我们会发现页面加载时会显示地图位置:
每次更改地址字段时,地图和纬度/经度坐标都会更新。
现在,地图显示“晚餐位置”,我们还可以将“纬度”和“经度”窗体字段从可见文本框更改为隐藏元素 (,因为地图会在每次输入地址时自动更新它们) 。 为此,我们将从使用 Html.TextBox () HTML 帮助程序切换到使用 Html.Hidden () 帮助程序方法:
<p>
<%= Html.Hidden("Latitude", Model.Dinner.Latitude)%>
<%= Html.Hidden("Longitude", Model.Dinner.Longitude)%>
</p>
现在,我们的表单更加用户友好,避免显示原始纬度/经度 (同时仍将它们与每个 Dinner 一起存储在数据库中) :
将地图与详细信息视图集成
现在,我们已将地图与创建和编辑方案集成,接下来让我们将其与详细信息方案集成。 只需在“详细信息”视图中调用 <%Html.RenderPartial (“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>
现在,当用户导航到 /Dinners/Details/[id] URL 时,他们将看到有关晚餐的详细信息,地图上的晚餐位置 (通过一个推送固定完成,将鼠标悬停在上方时会显示晚餐的标题和) 的地址,并为其提供指向 RSVP 的 AJAX 链接:
在数据库和存储库中实现位置搜索
为了完成 AJAX 实现,让我们向应用程序的主页添加一个 Map,允许用户以图形方式搜索他们附近的晚餐。
首先,我们将在数据库和数据存储库层中实现支持,以有效地执行基于位置的 Dinners 半径搜索。 我们可以使用 SQL 2008 的新地理空间功能 来实现此功能,或者可以使用 Gary Dryden 在此处的文章中讨论的 SQL 函数方法: http://www.codeproject.com/KB/cs/distancebetweenlocations.aspx。
若要实现此技术,我们将在 Visual Studio 中打开“服务器资源管理器”,选择 NerdDinner 数据库,然后右键单击其下的“functions”子节点,并选择创建新的“标量值函数”:
然后粘贴以下 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
然后,我们将在 SQL Server 中创建一个新的表值函数,我们将调用“NearestDinners”:
此“NearestDinners”表函数使用 DistanceBetween 帮助程序函数返回我们提供的纬度和经度 100 英里内的所有 Dinner:
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
若要调用此函数,我们将首先通过双击 \Models 目录中的 NerdDinner.dbml 文件来打开 LINQ to SQL 设计器:
然后,我们将 NearestDinners 和 DistanceBetween 函数拖到 LINQ to SQL 设计器上,这将导致将它们作为方法添加到 LINQ to SQL NerdDinnerDataContext 类中:
然后,我们可以在 DinnerRepository 类上公开一个“FindByLocation”查询方法,该方法使用 NearestDinner 函数返回距离指定位置 100 英里以内的即将到来的 Dinner:
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;
}
实现基于 JSON 的 AJAX 搜索操作方法
我们现在将实现一个控制器操作方法,该方法利用新的 FindByLocation () 存储库方法返回可用于填充地图的 Dinner 数据列表。 我们将让此操作方法以 JSON (JavaScript 对象表示法) 格式返回 Dinner 数据,以便在客户端上使用 JavaScript 轻松操作它。
为了实现此目标,我们将通过右键单击 \Controllers 目录并选择 Add-Controller> 菜单命令来创建新的“SearchController”类。 然后,我们将在新的 SearchController 类中实现“SearchByLocation”操作方法,如下所示:
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());
}
}
SearchController 的 SearchByLocation 操作方法在内部调用 DinnerRepository 上的 FindByLocation 方法以获取附近晚餐的列表。 不过,它不直接将 Dinner 对象返回给客户端,而是返回 JsonDinner 对象。 例如,JsonDinner 类公开 Dinner 属性的子集 (:出于安全原因,它不会公开具有晚餐) RSVP 的人员的姓名。 它还包括一个在 Dinner 上不存在的 RSVPCount 属性,该属性通过计算与特定晚餐关联的 RSVP 对象数来动态计算。
然后,我们将使用控制器基类上的 Json () 帮助程序方法,以使用基于 JSON 的线路格式返回晚餐序列。 JSON 是表示简单数据结构的标准文本格式。 下面是从操作方法返回的两个 JsonDinner 对象的 JSON 格式列表的示例:
[{"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}]
使用 jQuery 调用基于 JSON 的 AJAX 方法
我们现在已准备好更新 NerdDinner 应用程序的主页,以使用 SearchController 的 SearchByLocation 操作方法。 为此,我们将打开 /Views/Home/Index.aspx 视图模板并将其更新为具有文本框、搜索按钮、地图和 <名为 dinnerList 的 div> 元素:
<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>
然后,我们可以向页面添加两个 JavaScript 函数:
<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>
第一个 JavaScript 函数在页面首次加载时加载地图。 第二个 JavaScript 函数在搜索按钮上连接 JavaScript 单击事件处理程序。 按下按钮时,它会调用 FindDinnersGivenLocation () JavaScript 函数,我们将将其添加到Map.js文件:
function FindDinnersGivenLocation(where) {
map.Find("", where, null, null, null, null, null, false,
null, null, callbackUpdateMapDinners);
}
此 FindDinnersGivenLocation () 函数调用映射。在虚拟地球控件上查找 () ,使其在输入的位置上居中。 虚拟地球地图服务返回时,地图。Find () 方法调用我们作为最终参数传递的 callbackUpdateMapDinners 回调方法。
callbackUpdateMapDinners () 方法是完成实际工作的地方。 它使用 jQuery 的 $.post () 帮助程序方法对 SearchController 的 SearchByLocation () 操作方法执行 AJAX 调用 - 向其传递新居中地图的纬度和经度。 它定义了一个内联函数,该函数将在 $.post () 帮助程序方法完成时调用,并且将使用名为“dinners”的变量传递从 SearchByLocation () 操作方法返回的 JSON 格式晚餐结果。 然后,它会对每个返回的晚餐执行前移,并使用晚餐的纬度和经度以及其他属性在地图上添加新的图钉。 它还向地图右侧的晚餐的 HTML 列表添加晚餐条目。 然后,它会为图钉和 HTML 列表连接悬停事件,以便在用户将鼠标悬停在它们上方时显示有关晚餐的详细信息:
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");
现在,当我们运行应用程序并访问主页时,我们将看到一个地图。 当我们输入城市的名称时,地图将显示附近即将举行的晚餐:
将鼠标悬停在晚宴上将显示有关它的详细信息。
单击气泡或 HTML 列表中的右侧的“晚餐”标题将导航到晚餐- 然后,我们可以选择 RSVP:
下一步
现已实现 NerdDinner 应用程序的所有应用程序功能。 现在,让我们看看如何启用它的自动化单元测试。