Использование AJAX для реализации сценариев сопоставления

от Корпорации Майкрософт

Загрузить PDF-файл

Это шаг 11 бесплатного руководства по приложению "NerdDinner" , в которых показано, как создать небольшое, но полное веб-приложение с помощью ASP.NET MVC 1.

На шаге 11 показано, как интегрировать поддержку сопоставления AJAX в приложение NerdDinner, чтобы пользователи, которые создают, редактируют или просматривают ужины, графически видеть расположение ужина.

Если вы используете ASP.NET MVC 3, рекомендуем следовать руководствам по начало работы С MVC 3 или MVC Music Store.

Шаг 11 NerdDinner. Интеграция карты AJAX

Теперь мы сделаем наше приложение более наглядным, интегрируя поддержку сопоставления AJAX. Это позволит пользователям, которые создают, редактируют или просматривают ужины, чтобы увидеть расположение ужина графически.

Создание частичного представления карты

Мы будем использовать функции сопоставления в нескольких местах в нашем приложении. Чтобы сохранить наш код DRY, мы инкапсулируем общую функциональность карты в одном частичном шаблоне, который можно повторно использовать в нескольких действиях и представлениях контроллера. Мы назовем это частичное представление map.ascx и создадим его в каталоге \Views\Dinners.

Мы можем создать часть map.ascx, щелкнув правой кнопкой мыши каталог \Views\Dinners и выбрав команду меню Добавить-Вид>. Мы назовем представление 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"> — это контейнер HTML, который Virtual Earth будет использовать для размещения карты.

Затем у нас есть внедренный <блок скриптов> , содержащий две функции JavaScript, относящиеся к этому представлению. Первая функция использует jQuery для подключения функции, которая выполняется, когда страница готова к запуску клиентского скрипта. Он вызывает вспомогающую функцию LoadMap(), которую мы определим в файле скрипта Map.js для загрузки элемента управления картой виртуальной земли. Вторая функция — это обработчик событий обратного вызова, который добавляет закрепление на карту, идентифицирующее расположение.

Обратите внимание, что мы используем блок %= %> на стороне <сервера в блоке скрипта на стороне клиента для внедрения широты и долготы ужина, который мы хотим сопоставить в JavaScript. Это полезный метод вывода динамических значений, которые могут использоваться клиентским скриптом (без необходимости отдельного вызова AJAX на сервер для получения значений, что делает его быстрее). Блоки <%= %> будут выполняться при отрисовке представления на сервере, поэтому выходные данные HTML будут просто заканчиваться внедренными значениями JavaScript (например, var latitude = 47.64312;).

Создание библиотеки служебной программы Map.js

Теперь создадим файл Map.js, который можно использовать для инкапсуляции функциональных возможностей JavaScript для карты (и реализации описанных выше методов LoadMap и LoadPin). Это можно сделать, щелкнув правой кнопкой мыши каталог \Scripts в нашем проекте, а затем выбрать команду меню "Добавить новый> элемент", выбрать элемент JScript и присвоить ему имя "Map.js".

Ниже приведен код JavaScript, который мы добавим в файл Map.js, который будет взаимодействовать с Виртуальной Землей для отображения карты и добавления к ней закреплений расположений для наших ужинов:

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

Интеграция карты с созданием и изменением форм

Теперь мы интегрируем поддержку map с существующими сценариями создания и редактирования. Хорошая новость заключается в том, что это довольно легко сделать и не требует от нас изменения какого-либо кода контроллера. Так как в наших представлениях "Создание и изменение" используется общее частичное представление "DinnerForm" для реализации пользовательского интерфейса формы ужина, мы можем добавить карту в одном месте и использовать ее в сценариях создания и редактирования.

Все, что нам нужно сделать, это открыть частичное представление \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:

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

Функция JavaScript, добавленная в часть, использует jQuery для присоединения события размытия к текстовому полю "Адрес" HTML. Вы, вероятно, слышали о событиях фокуса, которые возникают, когда пользователь щелкает текстовое поле или нажимает его на вкладку. Наоборот, событие размытия возникает, когда пользователь выходит из текстового поля. Приведенный выше обработчик событий очищает значения в текстовом поле широты и долготы, а затем отображает новое расположение адреса на карте. Затем обработчик событий обратного вызова, определенный в файле map.js, обновит текстовые поля долготы и широты в нашей форме, используя значения, возвращаемые виртуальной землей на основе заданного нами адреса.

А теперь, когда мы снова запустим приложение и перейдем на вкладку Host Dinner, мы увидим карту по умолчанию вместе со стандартными элементами формы Dinner:

Снимок экрана: страница

При вводе адреса и переходе от табуляции карта будет динамически обновляться для отображения расположения, а наш обработчик событий заполняет текстовые поля широты и долготы значениями расположения:

Снимок экрана: страница

Если мы сохраним новый ужин, а затем снова откроем его для редактирования, мы обнаружаем, что расположение карты отображается при загрузке страницы:

Снимок экрана: страница

При каждом изменении поля адреса карта и координаты широты и долготы будут обновляться.

Теперь, когда на карте отображается расположение ужина, можно также изменить поля формы "Широта" и "Долгота" с видимых текстовых полей на скрытые элементы (так как карта автоматически обновляет их при каждом вводе адреса). Для этого мы переключимся с использования вспомогательной функции HTML Html.TextBox() на использование вспомогательного метода Html.Hidden():

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

Теперь наши формы немного удобнее и не отображают необработанные широты и долготы (сохраняя их с каждым ужином в базе данных):

Снимок экрана: карта на странице Nerd Dinners.

Интеграция карты с представлением сведений

Теперь, когда карта интегрирована с нашими сценариями создания и редактирования, давайте также интегрируем ее со сценарием сведений. Все, что нам нужно сделать, это вызвать <% 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>

И теперь, когда пользователь переходит по URL-адресу /Dinners/Details/[id], он увидит сведения о ужине, расположение ужина на карте (в комплекте с push-закреплением, который при наведении указателя мыши отображает название ужина и его адрес), а также ссылку AJAX на RSVP для него:

Снимок экрана: веб-страница

Реализация поиска расположения в базе данных и репозитории

Чтобы завершить реализацию AJAX, давайте добавим карту на домашнюю страницу приложения, которая позволяет пользователям графически искать ужины рядом с ними.

Снимок экрана: домашняя страница

Начнем с реализации поддержки на уровне базы данных и репозитория данных, чтобы эффективно выполнять поиск по радиусу на основе расположения для Dinners. Для реализации этой функции можно использовать новые геопространственные функции SQL 2008 или использовать подход к функциям SQL, который Гэри Драйден обсудил в статье здесь: http://www.codeproject.com/KB/cs/distancebetweenlocations.aspx.

Чтобы реализовать этот метод, мы откроем серверную Обозреватель в Visual Studio, выберите базу данных NerdDinner, а затем щелкните правой кнопкой мыши под ним вложенный узел "функции" и создадим новую функцию со скалярным значением:

Снимок экрана: серверная Обозреватель в Visual Studio. Выбрана база данных Nerd Dinner и выбран вложенный узел функций. Выделена скалярная функция с значением.

Затем мы вставляем следующую функцию 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, которую мы назовем "Ближайшие диннеры":

Снимок экрана: сервер S Q L. Table-Valued выделена функция.

Эта табличная функция NearestDinners использует вспомогательную функцию DistanceBetween для возврата всех ужинов в пределах 100 миль от широты и долготы, которые мы ее поставляем:

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

Чтобы вызвать эту функцию, сначала откройте конструктор LINQ to SQL, дважды щелкнув файл NerdDinner.dbml в каталоге \Models:

Снимок экрана: файл Nerd Dinner dot d b m l в каталоге Models.

Затем мы перетащите функции NearestDinners и DistanceBetween в конструктор LINQ to SQL, что приведет к добавлению их в качестве методов в нашем LINQ to SQL классе NerdDinnerDataContext:

Снимок экрана: функции

Затем мы можем предоставить метод запроса FindByLocation в классе DinnerRepository, который использует функцию БлижайшийDinner для возврата предстоящих dinners, которые находятся в пределах 100 миль от указанного расположения:

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

Реализация метода действия поиска AJAX на основе JSON

Теперь мы реализуем метод действия контроллера, который использует преимущества нового метода репозитория FindByLocation() для возврата списка данных Dinner, которые можно использовать для заполнения карты. Этот метод действия возвращает данные Dinner в формате JSON (нотация объектов JavaScript), чтобы ими можно было легко управлять с помощью JavaScript на клиенте.

Для реализации этого мы создадим класс SearchController, щелкнув правой кнопкой мыши каталог \Controllers и выбрав команду меню Добавить контроллер>. Затем мы реализуем метод действия SearchByLocation в новом классе SearchController, как показано ниже:

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

Метод действия SearchByLocation SearchController внутренне вызывает метод FindByLocation в DinnerRepository, чтобы получить список близлежащих ужинов. Вместо того, чтобы возвращать объекты Dinner непосредственно клиенту, он возвращает объекты JsonDinner. Класс JsonDinner предоставляет подмножество свойств Dinner (например, по соображениям безопасности он не раскрывает имена людей, у которых есть RSVP'd на ужин). Он также включает свойство RSVPCount, которое не существует в Dinner и которое динамически вычисляется путем подсчета количества объектов RSVP, связанных с определенным ужином.

Затем мы используем вспомогательный метод Json() в базовом классе Controller, чтобы вернуть последовательность ужинов с помощью проводного формата на основе JSON. JSON — это стандартный текстовый формат для представления простых структур данных. Ниже приведен пример того, как выглядит список в формате JSON из двух объектов JsonDinner при возврате из нашего метода действия:

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

Вызов метода AJAX на основе JSON с помощью jQuery

Теперь мы готовы обновить домашнюю страницу приложения NerdDinner, чтобы использовать метод действия SearchByLocation SearchController. Для этого мы откроем шаблон представления /Views/Home/Index.aspx и обновим его, чтобы в нем было текстовое поле, кнопка поиска, карта и <элемент div> с именем 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>

Затем на страницу можно добавить две функции 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 на кнопке поиска. При нажатии кнопки вызывается функция JavaScript FindDinnersGivenLocation(), которую мы добавим в файл Map.js:

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

Эта функция FindDinnersGivenLocation() вызывает сопоставление. Найдите() в виртуальном элементе управления Землей, чтобы вывести его по центру в указанное расположение. Когда служба карты виртуальной земли возвращается, карта. Метод Find() вызывает метод обратного вызова callbackUpdateMapDinners, который мы передали в качестве последнего аргумента.

Метод callbackUpdateMapDinners() — это место, где выполняется реальная работа. Он использует вспомогательный метод $.post() jQuery для выполнения вызова AJAX метода действия SearchByLocation() SearchController, передавая ему широту и долготу новой карты по центру. Он определяет встроенную функцию, которая будет вызываться после завершения вспомогательного метода $.post(), а результаты ужина в формате JSON, возвращаемые методом действия SearchByLocation(), будут переданы с помощью переменной с именем "dinners". Затем он делает foreach над каждым возвращенным ужином, и использует широту и долготу ужина и другие свойства, чтобы добавить новую булавку на карте. Он также добавляет запись ужина в HTML-список ужинов справа от карты. Затем он подключает событие наведении указателя как для pushpins, так и для 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");

А теперь, когда мы запустим приложение и перейдем на домашнюю страницу, нам будет представлена карта. Когда мы введем название города, на карте будут отображаться предстоящие ужины рядом с ним:

Снимок экрана домашней страницы Nerd Dinner с показанной картой.

При наведении курсора на ужин будут отображаться сведения об этом.

Щелкнув заголовок ужина в пузырьке или в правой части html-списка, мы перейдем к ужину, который затем можно дополнительно rsvp для:

Снимок экрана: страница сведений о ужине в Ней с картой, показывающая навигацию к ужину.

Следующий шаг

Теперь мы реализовали все функциональные возможности приложения NerdDinner. Теперь давайте рассмотрим, как включить автоматическое модульное тестирование.