使用 AJAX 實作對應實例
由 Microsoft 提供
這是免費的 "NerdDinner" 應用程式教學課程的第 11 個步驟,詳細介紹了如何使用 ASP.NET MVC 1 建置一個小型但完整的 Web 應用程式。
步驟 11 示範如何將 AJAX 對應支援整合到我們的 NerdDinner 應用程式中,讓建立、編輯或檢視 Dinners 的使用者以圖形方式查看 Dinner 的位置。
如果使用 ASP.NET MVC 3,建議遵循 MVC 3 使用者入門或 MVC Music 市集教學課程。
NerdDinner 步驟 11:整合 AJAX 對應
現在,我們將透過整合 AJAX 對應支援來使應用程式在視覺上更加令人興奮。 這可讓建立、編輯或檢視 Dinners 的使用者以圖形方式查看 Dinner 的位置。
建立地圖部分檢視
我們將在應用程式中的數個位置使用對應功能。 為了保持我們的程式碼 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 指令碼檔案中定義,以載入 Virtual Earth 地圖控制項。 第二個函式是回撥事件處理常式,會在地圖上新增一個圖釘,以標示某個位置。
請注意,我們在用戶端指令碼區塊中使用伺服器端 <%= %> 區塊,以內嵌我們想要對應至 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 互動以顯示我們的地圖,並為我們的 Dinners 新增位置圖釘:
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" 部分檢視來實作 Dinner 表單的 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 表單項目:
當我們輸入地址,然後選取索引標籤時,地圖會動態更新以顯示位置,而我們的事件處理常式會以位置值填入緯度/經度文字方塊:
如果我們儲存新的 Dinner,然後再次開啟它進行編輯,就會發現地圖位置會在頁面載入時顯示:
每次變更地址欄位時,地圖和緯度/經度座標都會更新。
現在地圖會顯示 Dinner 位置,我們也可以將緯度和經度表單欄位從可見文字方塊變更為隱藏項目 (因為地圖會在每次輸入地址時自動更新它們)。 若要這麼做,我們將從使用 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 時,他們會看到 Dinner 的詳細資料、地圖上的 Dinner 位置 (並包含一個圖釘圖示,當滑鼠懸停時會顯示 Dinner 的標題和地址),並有一個 AJAX 連結到 RSVP:
在我們的資料庫和存放庫中實作位置搜尋
為了完成 AJAX 實作,讓我們將 [地圖] 新增至應用程式的首頁,讓使用者以圖形方式搜尋附近的 Dinners。
我們一開始會在資料庫和資料存放庫層內實作支援,以有效率地執行以位置為基礎的 Dinners 半徑搜尋。 我們可以使用 SQL 2008 的新地理空間功能來實作這項功能,或者,也可以使用 Gary Dryden 在本文中討論的 SQL 函式方法:http://www.codeproject.com/KB/cs/distancebetweenlocations.aspx。
為了實作這項技術,我們會在 Visual Studio 中開啟 [伺服器總管],選取 NerdDinner 資料庫,然後在其下方的 [函式] 子節點上按一下滑鼠右鍵,然後選擇建立新的 [純量值函式]:
然後,我們會貼上下列 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 英里範圍內的所有 Dinners:
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 英里內即將推出的 Dinners:
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 目錄並選擇 [新增 -> 控制器] 功能表命令,以建立新的 “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 方法,以取得附近 Dinners 的清單。 不過,它不是直接將 Dinner 物件傳回給用戶端,而是傳回 JsonDinner 物件。 JsonDinner 類別會公開 Dinner 屬性的子集 (例如:出於安全原因,它不會透露已回覆 Dinner 的人員的姓名)。 它也包含一個不存在於 Dinner 上的 RSVPCount 屬性,其會藉由計算與特定 Dinner 相關聯的 RSVP 物件數目來動態計算。
接著,我們會在 Controller 基底類別上使用 Json() 協助程式方法,以使用 JSON 型 Wire 格式傳回 Dinners 的序列。 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 按一下事件處理常式。 按下按鈕時,它會呼叫我們將新增至 Map.js 檔案的 FindDinnersGivenLocation() JavaScript 函式:
function FindDinnersGivenLocation(where) {
map.Find("", where, null, null, null, null, null, false,
null, null, callbackUpdateMapDinners);
}
此 FindDinnersGivenLocation() 函式會呼叫地圖。在 [Virtual Earth 控制] 上尋找 () 將它置中於輸入的位置。 當虛擬地球地圖服務傳回時,map.Find() 方法會叫用我們作為最後一個引數傳遞的 callbackUpdateMapDinners 回撥方法。
callbackUpdateMapDinners() 方法是實際工作完成的位置。 它會使用 jQuery 的 $.post() 協助程式方法,對 SearchController 的 SearchByLocation() 動作方法執行 AJAX 呼叫 ,以傳遞最新置中地圖的緯度和經度。 它會定義內嵌函式,當 $.post() 協助程式方法完成時會呼叫,而 SearchByLocation() 動作方法傳回的 JSON 格式 Dinner 結果將會使用名為 “dinners” 的變數來傳遞。 然後,它會在每個傳回的 Dinner 上執行 foreach,並使用 Dinner 的緯度和經度和其他屬性在地圖上新增釘選。 它也會將 Dinner 項目新增至地圖右側的 Dinners HTML 清單。 然後,它會連接圖釘和 HTML 清單的滑鼠暫留事件,讓使用者將滑鼠停留在他們上方時,會顯示 Dinner 的詳細資料:
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");
現在,當我們執行應用程式並造訪首頁時,我們將會看到地圖。 當我們輸入城市名稱時,地圖將會顯示附近即將推出的 Dinners:
將滑鼠暫留在 Dinner 上會顯示有關它的詳細資料。
按一下泡泡中的 Dinner 標題或右側 HTML 清單中的標題都會將我們導覽到該 Dinner,然後我們可以選擇性地回覆:
後續步驟
我們現在已實作 NerdDinner 應用程式的所有應用程式功能。 現在讓我們看看如何啟用它的自動化單元測試。