Современные веб-технологии

Добавление динамических компонент в Интернет-магазин

Разбить на страницы
Показывать лекцию целиком

Цель практического занятия: Целью данного занятия является рассмотрение возможности использования языка JavaScript и технологии AJAX для создания визуальных эффектов, а также доступа к данным и их отображения в Интернет-магазине.

Файлы к практическому занятию Вы можете скачать здесь.

15.1. Введение

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

Серверные функции ASP.NET могут быть дополнены функциональностью клиентских сценариев в веб-страницах ASP.NET. Клиентский сценарий можно использовать для предоставления пользователям более функционального и интерактивного интерфейса. Также с помощью клиентского сценария можно осуществлять асинхронные вызовы к веб-серверу во время выполнения страницы в веб-обозревателе.

ASP.NET предусматривает два подхода к использованию клиентских возможностей. Первый подход – это использование AJAX -функций платформы ASP.NET. Технология AJAX в ASP.NET включает библиотеку функций клиентских сценариев, привнося многие из преимуществ объектно-ориентированного программирования и моделей ASP.NET в клиентские сценарии. AJAX в ASP.NET также обеспечивает серверную поддержку, что включает серверные веб-элементы управления, автоматически отрисовывающие необходимый для функциональности AJAX клиентский сценарий.

Второй подход – создание клиентского сценария и самостоятельная интеграция его в веб-страницы ASP.NET. Этим подходом удобно пользоваться, если фрагменты клиентского сценария очень невелики или если требуется интегрировать существующий клиентский сценарий в приложение. Данный подход используется многими серверными веб-элементами управления ASP.NET (например, проверяющими элементами управления) для создания клиентской функциональности.

15.2. Применение AJAX

Прежде всего, рассмотрим применение технологии AJAX. Для этого в мастер страниц в самое начало содержимого формы (тег form ) необходимо добавить следующий код:

<asp:scriptmanager ID="Scriptmanager1" runat="server"></asp:scriptmanager>

Этот объект будет отвечать за работу JavaScript, в частности за подключение нового скриптового кода, который будет скачиваться при асинхронных запросах.

Также добавим еще один ContentPlaceHolder, который будем использовать как контейнер для JavaScript функций на различных страницах.

<asp:ContentPlaceHolder ID="PlaceHolderScripts" runat="server"> 
</asp:ContentPlaceHolder>

15.2.1. Подключение клиентских сценариев

Теперь в мастере страниц подключим известную JavaScript-библиотеку jquery (скачать последнюю версию библиотеки и почитать документацию можно по адресу: http://jquery.com/):

<script type="text/javascript" src="JS/jquery-1_2_6_min.js"></script>

Перейдем на главную страницу. Воспользовавшись библиотекой jquery, анимируем основной текст страницы, так чтобы он медленно всплывал, и сделаем так, чтобы надпись "Новинки!" мигала. Для этого добавим в PlaceHolderScripts следующий код:

<asp:Content ID="ContentScript" ContentPlaceHolderID="PlaceHolderScripts" runat="server">

    <script type="text/javascript">
        jQuery(document).ready(function() {
            $('#MainText').toggle(1000);
            $('#News').toggleClass('h3').toggle(1000, function() {
                animateText();
            });

        });

        function animateText() {
            $('#News').toggleClass('h3').toggle(1000, function() {
                animateText();
            });
        }
    </script>. 
</asp:Content>

Здесь определены две функции. Первая, анонимная функция, сработает в тот момент, когда страница будет сформирована браузером. Так как изначально основному тексту присвоен CSS-класс text-init, который содержит правило display:none, то при загрузке страницы этот текст не отобразится. $('#MainText') найдет элемент с идентификатором MainText, а метод toggle(1000) изменит видимость этого элемента (в данном случае – покажет его), причем процесс отображения текста будет анимирован и займет 1 секунду.

Для элемента с идентификатором News, будет поочередно "включаться" и "выключаться" применение стиля h3, а функция toogle будет скрывать и показывать надпись, создавая эффект мигания. Здесь используется перегрузка функции toggle, которая отработав в течение секунды, вызовет функцию animateText. Эта функция, в свою очередь, снова изменит видимость надписи и вызовет себя.

Результат добавления визуальных эффектов представлен на рис. 15.1.

(рис 15.1) Главная страница сайта с "всплывающим" текстом

Теперь перейдем к рассмотрению возможностей технологии AJAX. Основная задача, которую она решает, заключается в асинхронном обмене данными с сервером. Это значит, что действия пользователя не приводят к перезагрузке страницы. На самом деле данные отправляются и получаются с сервера JavaScript -объектом XMLHttpRequest, который поддерживается всеми современными браузерами. При это в технологии AJAX ASP.NET, обращение XMLHttpRequest к серверу называется асинхронным "постбэком" (AsyncPostBack) и так же, как и в случае обычного "постбэка", приводит к восстановлению всей страницы на сервере. Это обеспечивается тем, что содержимое ViewState передается на сервер и при асинхронных запросах.

Так как данные при использовании AJAX передаются независимо от страницы, возникает задача обработки полученного от сервера ответа и отображения его на странице. В общем случае, она решается также применением языка JavaScript. Разработчик пишет клиентский код, который разбирает полученные данные и на его основе генерирует нужный для отображения HTML. В ASP.NET AJAX предоставляется намного более простой способ решения этой проблемы, который заключается в использовании серверного компонента UpdatePanel.

15.2.2. Работа с UpdatePanel

При первом рассмотрении UpdatePanel сходен в работе с обычным Panel. В результате рендеренга страницы этот компонент также заменяется на HTML-элемент div и служит для группировки и разметки страницы. Особенность же этого компонента заключается в том, что любой запрос, совершенный элементом, расположенным внутри UpdatePanel является асинхронным и приводит только к перерисовке его содержимого. Остальная страница не изменяется. Даже если при постбэке пользователь изменит структуру или набор компонент вне UpdatePanel, то на этапе Render жизненного цикла страницы, эти изменения не будут учтены, произойдет генерация HTML-разметки только для содержимого компонента и эта разметка будет отправлена клиенту. Клиентская часть ASP.NET AJAX обработает полученный код и изменить разметку внутри div' а, соответствующего UpdatePanel.

Таким образом, расположив на странице несколько UpdatePanel' ов (а их можно расположить любое количество, вкладывая, например, один в другой) можно реализовать интерфейс, который никогда не будет перерисовывать страницу и в то же время предоставлять все функциональные возможности сайта.

Иногда бывает не достаточно обновлять только содержимое того UpdatePanel' а, который вызвал постбэк. Нужно заставить обновиться и другие UpdatePanel' ы на странице. Это можно сделать двумя способами:

  • В разметке ASP или в code-behind UpdatePanel указать триггеры ( Triggers ), которые будут приводить к перерисовке содержимого этой панели. В качестве параметров указываются идентификаторы серверных компонент, находящихся вне UpdatePanel, которые должны привести к обновлению его содержимого.
  • В code-behind'e можно вызвать метод UpdatePanel.Update(), что приведет к тому, что его содержимое будет ренедрится на сервере и отправляться клиенту.
  • Все это значит, что для внедрения AJAX в наш Интернет-магазин достаточно поместить все необходимые компоненты в UpdatePanel' ы и при необходимости указать триггеры:

    <asp:UpdatePanel ID="UpdatePanelComments" 
           runat="server" UpdateMode="Conditional">
      <Triggers>
        <asp:AsyncPostBackTrigger ControlID="AddReview"  /> 
      </Triggers>
      <ContentTemplate>
        <asp:DataList ID="ReviewList" runat="server" Width="100%" 
            CellPadding="0" CellSpacing="0">
          <ItemTemplate>
            <asp:Label ID="Label1" style='font-weight:bold' 
    	      Text='<%# DataBinder.Eval(Container.DataItem, "ReviewerName") %>'
                  runat="server" />
            <span class="Normal">говорит... </span>
            <asp:Image ID="Image1" runat="server" 
    	      ImageUrl='<%# "~/images/ReviewRating" + DataBinder.Eval(Container.DataItem, "Rating") +".gif" %>' />
            <br>
            <asp:Label ID="Label2" CssClass="Normal" 
                 Text='<%# DataBinder.Eval(Container.DataItem, "Comments") %>'
                 runat="server" />
          </ItemTemplate>
          <SeparatorTemplate>
            <br>
          </SeparatorTemplate>
        </asp:DataList>
      </ContentTemplate>
    </asp:UpdatePanel>
    …
    <asp:UpdatePanel ID="UpdatePanelNewCommentProperties" runat="server" 
         UpdateMode="Conditional">
      <Triggers><asp:AsyncPostBackTrigger ControlID="AddReview"  /> 
      </Triggers>
      <ContentTemplate>
        …
      </ContentTemplate>
    </asp:UpdatePanel>
    
    …
    <asp:UpdatePanel ID="UpdatePanelCommentButton" runat="server">
      <ContentTemplate>
        <br>
        <span class="NormalBold">Комментарий</span>
        <br>
        <asp:TextBox ID="Comment" TextMode="multiline" 
            MaxLength="3850" Rows="7" Width="100%"
            runat="server" />
        <asp:RequiredFieldValidator ControlToValidate="Comment" 
            Display="Dynamic" Font-Names="verdana" Font-Size="9pt" 
            ErrorMessage="Поле 'Комментарий' должно быть заполнено." runat="server"
            ID="RequiredFieldValidator3"></asp:RequiredFieldValidator>
        <br>
        <br>
        <asp:Button ID="AddReview" runat="server" 
            OnClick="OnClick" Text="Добавить свой комментарий" />
        <br>
      </ContentTemplate>
    </asp:UpdatePanel>

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

    15.3. Работа с веб-службами в ASP.NET AJAX

    Теперь, когда мы научились работать с языком JavaScript и сумели оптимизировать работу сайта при помощи ASP.NET AJAX стоит оценить преимущества и недостатки текущей реализации Интернет-магазина. Нам удалось сократить время работы серверного кода (хотя и незначительно) за счет того, что мы используем UpdatePanel, и теперь метод Page.Render отрисовывает не всю странице целиком, а только определенные части. Более того, уменьшился и объем данных, передаваемый от сервера клиенту, опять же за счет того, что передается не вся страница. Второе преимущество, которого нам удалось добиться, заключается в том, что пользователь не наблюдает процесс перезагрузки всей страницы при каждой обратной передаче, что делает пользовательский интерфейс страницы более естественным.

    Среди недостатков стоит отметить тот факт, что каждое действие пользователя приводит к отправке на сервер и получение от него большого объема данных в виде ViewState. Если же отказаться от использования ViewState, то для восстановления страницы на сервере необходимо либо хранить все данные в сессии, либо каждый раз извлекать их из базы данных. Еще один недостаток заключается в том, что при использовании достаточно сложного интерфейса, насыщенного большим количеством сложных компонент, время их рендеринга на сервере сильно возрастает.

    Один из подходов к решению этих проблем заключается в использовании веб-сервисов. Вместо того чтобы каждый раз ради отправки или получения данных посылать или получать всю страницу целиком, можно сделать так, чтобы клиентский код JavaScript обращался на сервер, получал необходимые данные и сам же их отображал. Этот подход серьезно сократит трафик между сервером и клиентом (в случае с таблицей продуктов, о которой в дальнейшем пойдет речь, вместо 17-18 килобайт данных будет передаваться 1 килобайт) и снизит нагрузку на сервер, так как теперь его основные обязанностями будут заключаться в том, чтобы манипулировать данными.

    При этом, однако, многократно возрастет нагрузка на клиент. Это может привести к тому, что данный подход станет неприменим в случаях, когда у клиентов слабые компьютеры или на них установлены старые браузеры, плохо поддерживающие JavaScript.

    Тем не менее, для сайта, с которыми работает одновременно множество людей, этот подход способен серьезно улучшить производительность приложения.

    15.3.1. Создание веб-службы

    Для того чтобы добавить веб-сервис в проект, необходимо кликнуть правой кнопкой мыши по проекту в окне Solution Explorer и выбрать раздел меню Add New Item. В открывшемся диалоговом окне, необходимо выбрать элемент Web Service (рис. 15.2).

    (рис 15.2) Окно добавления нового веб-сервиса в проект

    В результате в проект будет добавлено два файла: WebProductService.asmx и WebProductService.cs, причем последний фал будет помещен в директорию App_code.

    Чтобы разрешить вызов веб-служб (ASMX) из клиентского сценария на веб-странице ASP.NET, необходимо добавить на страницу элемент управления ScriptManager. Чтобы определить ссылку на веб-службу, необходимо добавить дочерний элемент asp:ServiceReference к элементу управления ScriptManager. После этого необходимо установить URL-адрес веб-службы в качестве значения атрибута ссылки на сервер path. Объект ServiceReference определяет необходимость создания прокси-класса JavaScript для вызова указанной веб-службы в ASP.NET.

    Так как в нашем случае ScriptManager определен на мастере страниц, изменим его код следующим образом:

    <asp:ScriptManager ID="Scriptmanager1" runat="server">
                <Services>
                    <asp:ServiceReference Path="~/WebProductService.asmx" />
                </Services>
    </asp:ScriptManager>

    Теперь необходимо реализовать код этого сервиса. Для примера сделаем так, чтобы сервис мог возвращать список продуктов с учетом указанной категории и подкатегории и при этом поддерживал постраничный вывод.

    Прежде чем приступить к реализации самого сервиса, напишем два вспомогательных класса. Первый класс ProductDTO (DTOData Transfer Object) будет представлять собой описание структуры данных продукта, которую сервис будет отправлять клиенту.

    [Serializable]
    public class ProductDTO
    {
        public int ProductID { get; set; }
        public string ProductNumber { get; set; }
        public string Name { get; set; }
        public string Color { get; set; }
        public decimal ListPrice { get; set; }
        public string FullSize { get; set; }
        public string Weight { get; set; }
        
        public ProductDTO(Product p)
        {
            ProductID = p.ProductID;
            ProductNumber = p.ProductNumber;
            Name = p.Name;
            Color = p.Color;
            ListPrice = p.ListPrice;
            FullSize = p.FullSize;
            Weight = p.Weight + p.WeightUnitMeasureCode;
        }
    
        public ProductDTO()
        {
            
        }
    }
    Примечание 1: Атрибут [Serializable] в данном случае необязателен. Он показывает, что объекты этого классы должны поддерживать возможность представляться в виде строки, которую можно передать от одного сервиса к другому так, чтобы получающий сервис смог восстановить передаваемый объект. Сериализовать объект можно в xml -файл, json -объект или в бинарный код. Примечание 2: так как данный класс сериализуем, необходимо чтобы у него был определен публичный конструктор без параметров. Примечание 3: нам необходимо разработать вспомогательные классы, так как класс Product не сериализуем. Это связано с тем, что сериализации подвергаются все public свойства, среди которых есть ссылки на объекты, которые также рекурсивно сериализуются. Так, например, Product имеет ссылку ProductSubcategory на свою подкатегорию, которая в свою очередь имеет ссылку Products. В результате при сериализации произойдет зацикливание.

    Второй класс также будет использоваться для передачи информации клиенту. Он состоит из двух свойств:

  • Result предназначается для передачи коллекции ProductDTO, которая удовлетворяет текущим критериям поиска;
  • TotalCount содержит общее количество продуктов, которые соответствуют критериям поиска.
  • Этот класс позволит организовать постраничный вывод продуктов. Ниже представлен код этого класса:

    public class ResultStructure
    {
        public object Result{get;set;}
        public int TotalCount { get; set; }
    }

    Теперь, когда все вспомогательные структуры готовы, необходимо определить сервис, который будет обрабатывать запросы:

    /// <summary>
    /// Summary description for WebProductService
    /// </summary>
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    // To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line. 
    [System.Web.Script.Services.ScriptService]
    public class WebProductService : System.Web.Services.WebService {

    Этот код будет автоматически добавлен Visual Studio, при создании сервиса. Нам необходимо разкомментировать атрибут ScriptService, так как он позволяет вызвать сервис из JavaScript.

    Дальше в самом классе реализуются доступные для вызова веб-методы. Все такие методы помечаются атрибутом WebMethod. В нашем примере мы разработаем только один метод GetProducts, который на вход будет получать текущую категорию, подкатегорию и номер страницы:

    [WebMethod]
    [ScriptMethod(ResponseFormat = ResponseFormat.Json)]
    public ResultStructure GetProducts(string category, string subcategory, int skip)
    {
      DataClassesDataContext dcdc =
        new DataClassesDataContext(
            "Data Source=localhost;Initial Catalog=AdventureWorks;Integrated Security=True");
      var query = from p in dcdc.Products select p;
      if (!string.IsNullOrEmpty(subcategory))
      {
        query = query.Where(p => p.ProductSubcategoryID == Convert.ToInt32(subcategory));
          
      }
      else
      {
        if (!string.IsNullOrEmpty(category))
        {
          query = query.Where(p => p.ProductSubcategory.ProductCategoryID == Convert.ToInt32(category));
        }
      }
      return new ResultStructure()
      {
        Result = query.Skip(skip).Take(10).Select(p => new ProductDTO(p)).ToList(),
        TotalCount = query.Count()
      };
    }

    Код веб-метода идентичен коду, который разрабатывался на предыдущих занятиях для заполнения GridView данными о продуктах, только теперь метод не привязывает данные к какому-либо серверному компоненту, а возвращает сериализованные данные состоящие из коллекции продуктов и их общего количества. В атрибуте ScriptMethod указан параметр ResponseFormat = ResponseFormat.Json, который указывает, что результат необходимо сериализовать в формате JSON. Мы выбрали этот формат, так как он является основным формат представления данных для языка JavaScript.

    Если вызвать наш сервис и передать ему параметры, то будет получен ответ, аналогичный приведенному:

    {"d":{"__type":"ResultStructure",
            "Result":
                   [{"ProductID":982,"ProductNumber":"BK-M38S-42",
    	             "Name":"Mountain-400-W Silver, 42",
    			     "Color":"Silver","ListPrice":769.4900,
    			     "FullSize":"42 CM ","Weight":"27,13LB "},
    			    {"ProductID":983,"ProductNumber":"BK-M38S-46",
    			     "Name":"Mountain-400-W Silver, 46",
    			     "Color":"Silver","ListPrice":769.4900,
    			     "FullSize":"46 CM ","Weight":"27,42LB "},
    			    {"ProductID":984,"ProductNumber":"BK-M18S-40",
    			     "Name":"Mountain-500 Silver, 40",
    			     "Color":"Silver","ListPrice":564.9900,
    			     "FullSize":"40 CM ","Weight":"27,35LB"},
    			    {"ProductID":985,"ProductNumber":"BK-M18S-42",
    			     "Name":"Mountain-500 Silver, 42",
    			     "Color":"Silver","ListPrice":564.9900,
    			     "FullSize":"42 CM ","Weight":"27,77LB "},
    			    {"ProductID":986,"ProductNumber":"BK-M18S-44",
    			     "Name":"Mountain-500 Silver, 44",
    			     "Color":"Silver","ListPrice":564.9900,
    			     "FullSize":"44 CM ","Weight":"28,13LB "},
    			    {"ProductID":987,"ProductNumber":"BK-M18S-48",
    			     "Name":"Mountain-500 Silver, 48",
    			     "Color":"Silver","ListPrice":564.9900,
    			     "FullSize":"48 CM ","Weight":"28,42LB "},
    			    {"ProductID":988,"ProductNumber":"BK-M18S-52",
    			     "Name":"Mountain-500 Silver,52",
    			     "Color":"Silver","ListPrice":564.9900,
    			     "FullSize":"52 CM ","Weight":"28,68LB "},
    			    {"ProductID":989,"ProductNumber":"BK-M18B-40",
    			     "Name":"Mountain-500 Black, 40",
    			     "Color":"Black","ListPrice":539.9900,
    			     "FullSize":"40 CM ","Weight":"27,35LB "},
    			    {"ProductID":990,"ProductNumber":"BK-M18B-42",
    			     "Name":"Mountain-500 Black, 42",
    			     "Color":"Black","ListPrice":539.9900,
    			     "FullSize":"42 CM ","Weight":"27,77LB "},
    			    {"ProductID":991,"ProductNumber":"BK-M18B-44",
    			     "Name":"Mountain-500 Black, 44",
    			     "Color":"Black","ListPrice":539.9900,
    			     "FullSize":"44 CM ","Weight":"28,13LB "}],
            "TotalCount":32}}

    При этом для сервиса будут доступны страницы, на которых будут представлены описание методов, которые предоставляет сервис, а также тестовые страницы, на которых можно вызвать метод, передав параметры. Впрочем, в случае использования JSON-формата данных, вызвать методы не получится, так как это приведет к ошибке. Примеры таких страниц представлены на рис. 15.3 и рис. 15.4.

    (рис 15.3) Страница с описанием сервиса(рис 15.4) Тестовая страница для операции GetProduct сервиса WebProductService

    15.3.2. Вызов веб-службы из клиенсткого сценария

    Теперь, когда веб-сервис реализован, можно переделать страницу products/default.aspx, сделав так, чтобы она отображала данные, которые будет возвращать веб-сервис. Чтобы облегчить работу с отображением данных, воспользуемся библиотекой ExtJS, ознакомиться и скачать которую можно по адресу: http://www.extjs.com/

    Примечание: библиотека ExtJS является коммерческим продуктом. Узнать о правилах лицензирования можно по адресу: http://www.extjs.com/products/license.php

    Подключим библиотеку ExtJS к нашему проекту, добавив ссылки на файлы ext-all.css, ext-base.js и ext-all.js в head' е мастера страниц так, как это указано в следующем коде:

    <head runat="server">
      <meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
      <title></title>
      <link href="styles/style2.css" media="screen" rel="stylesheet" title="CSS" type="text/css" />
      <link rel='stylesheet' type='text/css' href='styles/ext-all.css' />
    
      <script type="text/javascript" src="../JS/ext-base.js" ></script>
    
      <script type="text/javascript" src="../JS/ext-all.js" ></script>
    
      <script type="text/javascript" src="JS/jquery-1_2_6_min.js"></script>
    
    </head>

    Теперь перейдем к странице products/default.aspx, удалим код, содержащийся в ContentPlaceHolder' ах и серверный код.

    Теперь в левый ContentPlaceHolder добавим тег script, где разместим следующий код:

    Ext.onReady(function(){
       var query = getQueryVariable();
       var result = WebProductService.GetProducts
        (query.category ? query.category : '', 
         query.subCategory ? query.subCategory : '',0, SucceededCallback, FailedCallback);
      });
      function FailedCallback(error) {
        alert(error.message);
      }
      function SucceededCallback(result, eventArgs) {
      }
      function getQueryVariable() {
        //полачаем строку запроса (?a=123b=qwe) и удаляем знак ?
        var query = window.location.search.substring(1);
        //получаем массив значений из строки запроса вида vars[0] = 'a=123';
        var vars = query.split("");
        var arr = new Array();
        //переводим массив vars в обычный ассоциативный массив 
        for (var i = 0; i < vars.length; i++) {
          var pair = vars[i].split("=");
          arr[pair[0]] = pair[1];
        }
        return arr;
    }

    Функцию SucceededCallback на данный момент определять не будем, так как прежде нам потребуется написать вспомогательный код.

    В приведенном коде определены 4 функции. Первая функция является анонимной, и выполняется в тот момент, когда библиотека ExtJS закончит инициализацию, что произойдет после полной отрисовки страницы. Вначале будет вызвана вспомогательная функция getQueryVariable, которая позволит определить, какие переменные определены в строке запроса.

    Функция getQueryVariable обращается к объекту window.location, который содержит всю строку запроса, после чего обращается в свойству search, содержащему строку параметров в формате ?a=123b=qwe. Далее у этой строки удаляется первый символ, и она методом split разделяется на множество строк, разделенных знаком . После этого, каждая полученная строка также разделяется на две, содержащие имя параметра и его значение, и записываются в массив arr, который в данном случае выступает в качестве хэш-таблицы.

    Результат вызова функции getQueryVariable помещается в переменную query.

    Далее вызывается функция WebProductService.GetProducts, которая представляет собой сгенерированную ScriptManager' ом прокси-функцию, вызывающую одноименный веб-метод. При этом JavaScript устроен так, что вызов самого веб-метода будет отложен до тех пор, пока текущий поток не закончит свою работу. Чтобы узнать, как отработал веб-метод, в функцию WebProductService.GetProducts помимо трех параметров, необходимых для работы, передаются также два метода обратного вызова. Метод FailedCallback будет вызван в случае, если во время выполнения веб-метода произойдет ошибка, и отобразит текст этой ошибки пользователю. Метод SucceededCallback выполнится в случае, если веб-метод успешно завершит свое выполнение. В этом случае переменная result будет содержать результат работы веб-метода.

    Уже сейчас можно запустить наш сайт и, если открыть IE Developer Tools и поставить breakpoint в метод SucceededCallback, можно убедиться, что веб-сервис возвращает нужный ответ, изучив переменную result (рис. 15.5).

    (рис 15.5) Переменная result в IE Developer Tools

    Однако данные будут бесполезны, если пользователь их не увидит. Для того чтобы отобразить данные, воспользуемся клиентским компонентом Ext.grid.GridPanel, который предоставляет во многом схожую функциональность с той, что дает ASP.NET -компонент GridView, но позволяет выполнять большинство действий без взаимодействия с сервером, не перерисовывая страницу.

    Как и для ASP.NET GridView необходимо для Ext.grid.GridPanel указать источник данных, который должен наследоваться от класса Ext.data.Store. Так как разработанный веб-метод возвращает данные в формате JSON, то нам подойдет класс Ext.data.JsonStore. Каждый источник данных в этом подходе состоит из двух важных компонент: DataProxy, который отвечает за извлечение данных, и Reader' а, который умеет полученные при помощи DataProxy данные разбирать. И если стандартный класс JsonReader нас устроит для разбора данный, то ни одна из реализаций DataProxy не позволяет обращаться к веб-сервисам. Поэтому мы разработаем собственный (сильно упрощенный) прокси-класс, который назовем Ext.data.FunctionProxy:

    Ext.data.FunctionProxy = function(f) {
       Ext.data.FunctionProxy.superclass.constructor.call(this);
       this.func = f;
    };
    
    Ext.extend(Ext.data.FunctionProxy, Ext.data.DataProxy, {
       load: function(params, reader, loadCallback, scope, arg) {
       var proxy = this;
       this.func({
          first: params.start,
          count: params.limit,
          sortAsc: params.dir != "DESC",
          scope: params.scope,
          totalCount: proxy.totalCount || 0,
          fields: params.fields
       },
       function(data) {
          var records = reader.readRecords(data);
          proxy.totalCount = records.totalRecords;
          proxy.lastArguments = proxy.arguments;
          loadCallback.call(scope, records, arg, true);
       });
     },
      reload: function() {
       this.load.apply(this, this.lastArguments);
      }
    })

    При разработке этого класса используется объектно-ориентированный подход программирования на JavaScript, предлагаемый библиотекой ExtJS. Вначале объявляется конструктор класса Ext.data.FunctionProxy, который принимает на вход переменную-функцию f и который вызывает конструктор базового класса. При этом переменная f сохраняется в поле func текущего объекта.

    Далее определяется, что класс Ext.data.FunctionProxy является расширением класса Ext.data.DataProxy, у которого переопределены методы load и reload. Это делается при помощи статического метода Ext.extend.

    Примечание: использование методов Ext в данном случае позволяет создать подход к программированию, соответствующий традициям ООП. Те же действия можно выполнить, не привлекая объектно-ориентированный код стандартными возможностями JavaScript, однако в этом случае сильно упадет наглядность кода.

    Функция load вызывает функцию func, передавая параметры в виде объекта arg, поля которого содежат информацию о номере первой извлекаемой запись, количестве записей, направлении сортировки, области видимости, полном количестве записей и наборе извлекаемых полей. Второй передаваемый в функцию func параметр представляет собой функцию обратного вызова, которая сработает, когда данные с сервера будут извлечены (т.е. заменит функцию SucceededCallback ). Эта функция применяет JsonReader для разбора записей и представления их в виде, понятном различным компонентам, включая Ext.grid.GridPanel, а также сохраняет информацию об общем количестве записей и аргументах вызова. Далее вызывается функция loadCallback, которая и сообщит связанным с этим источником данных компонентам, что в него загружены новые данные.

    Функция reload просто повторно вызывает функцию load с последними использованными параметрами.

    Последнее, что осталось подготовить – это место, где будет отрисовываться таблица. Для этого добавим ASP-панель на страницу products/default.aspx:

    <asp:Panel ID="PanelGrig" runat="server" Style="width:100%;" >
    
    </asp:Panel>

    Теперь приступим к реализации функции SucceededCallback. Для простоты, разобьем ее код на несколько частей:

    var storeProducts = new Ext.data.JsonStore({
        root: 'Result', remoteSort: true,
        fields: ['ProductID', 'ProductNumber', 'Name', 
                 'Color', 'ListPrice', 'FullSize', 'Weight'],
        totalProperty: 'TotalCount',
        proxy: new Ext.data.FunctionProxy(function(arg, loadCallback)
        {
          var query = getQueryVariable();
          var query = getQueryVariable();
          WebProductService.GetProducts(query.category ? query.category : '', 
                                        query.subCategory ? query.subCategory : '', 
                                        arg.first, loadCallback);
        })
    });
    storeProducts.load({
       params: {
          start: 0,
          limit: 10
       }
    });

    Здесь мы определяем источник данных storeProducts типа Ext.data.JsonStore, который будем использовать в дальнейшем. При этом указывается, что записи будут храниться в поле Result, а общее количество записей в поле TotalCount возвращаемого веб-сервисом JSON-объекта. Указывается, что сортировка записей проводится на сервере, определяется набор полей, которые будут храниться в источнике данных, и указывается прокси-класс. Здесь мы воспользуемся новым классом Ext.data.FunctionProxy, а передаваемая в него функция, повторяет код анонимной функции, рассмотренной ранее. После этого вызывается функция load источника данных, хотя это не обязательно.

    Далее определяется div, в который будет отрисована таблица. Здесь стоит обратить внимание, на то, что в качестве идентификатор при поиске элемента на странице используется не ID ASP-панели, а ее ClientId, которое генерируется ASP.NET автоматически. Данный подход, когда идентификатор "зашит" в код не очень хорош, так как идентификатор может поменяться, если будет изменена сама страница ASP. Но так как у нас всего один ASP-компонент на странице, то этим можно пренебречь:

    var el = 
    document.getElementById('ctl00_column_l_placeholder_PanelGrig');
    Теперь осталось отобразить саму таблицу:
                var grid = new Ext.grid.GridPanel({
                    renderTo: el,
                    stripeRows: true,
                    store: storeProducts,
                    colModel: new Ext.grid.ColumnModel({
                        defaults: {
                            width: 120,
                            sortable: true
                        },
                        columns: [
                        { header: 'Номер продукта', dataIndex: 'ProductNumber' },
                        { header: 'Название', dataIndex: 'Name' },
                        { header: 'Цвет', dataIndex: 'Color' },
                        { header: 'Цена', dataIndex: 'ListPrice' },
                        { header: 'Размер', dataIndex: 'FullSize' },
                        { header: 'Вес', dataIndex: 'Weight' }
                
                    ]
                    }),
                    viewConfig: {
                        forceFit: true
                    },
                    sm: new Ext.grid.RowSelectionModel({ singleSelect: true }),
                    width: 600,
                    height: 300,
                    frame: true,
             bbar: new Ext.PagingToolbar(
                {
                    pageSize: 10,
                    store: storeProducts,
                    listeners:
                    {
                        beforechange: function(sender, e) {
                        sender.store.reload(
                                {
                                    params:
                                    {
                                        start: e.start,
                                        limit: e.limit,
                                        scope: this
                                    }
                                });
                            return false;
                        }
                    }
                })
                });
            }

    Отметим только основные моменты этого кода. Компоненту указывается источник данных storeProducts, а также элемент el, в который этот компонент должен быть отрисован. Также указана модель столбцов, в которой определены все колонки, их заголовки и связь с полем источника данных. Также для нижней панели таблицы ( bbar ) определен компонент Ext.PagingToolbar, который обеспечивает возможность переключения страниц. Для этого в параметрах конструктора указан тот же источник данных, что и у таблицы, определен размер страницы, а в поле listeners определен обработчик клиентского события beforechange, в котором у источника данных вызывается метод reload, но уже для новой страницы (указывается новый стартовый элемент). Строчка return false ; позволяет избежать вызова других обработчиков, прикрепленных к событию beforechange пэйджера.

    Теперь все готово, и мы можем запустить наше веб-приложение и убедиться, что все работает. Примеры получившихся страниц представлены на рис. 15.6, рис. 15.7 и рис. 15.8.

    (рис 15.6) Первая страница списка всех продуктов(рис 15.7) 34-ая страница списка всех продуктов(рис 15.8) Список всех горных велосипедов

    Рассмотренный подход, обладает множеством преимуществ, однако также необходимо указать и недостатки такого подхода. Прежде всего, отметим, что теряется возможность визуально верстать страницу, так как весь код генерируется JavaScript (некоторые работы в решении этой проблемы ведутся, например: http://www.extjs.com/products/designer/). Разработка клиентских скриптов достаточно слабо поддерживается в Microsoft Visual Studio 2008. В этой области некоторые улучшения ожидаются от новой среды разработки Microsoft Visual Studio 2010. Еще одна проблема – это плохая интеграция ASP.NET и клиентских визуальных компонент. Здесь также есть наработки, в частности проект JASP (http://jaspproject.ru и http://code.google.com/p/jaspproject/) предлагает расширение для Visual Studio 2008, которое позволит разрабатывать клиентские компоненты в стиле ASP.NET. Это позволяет с одной стороны полностью отказаться от перезагрузки страниц, и использовать исключительно веб-сервисы как средство взаимодействие с сервером, а с другой – интегрировать веб-методы напрямую в компоненты и вызвать их из JavaScript кода как обычные скриптовые функции, что свойственно серверным компонентам. Подробнее о технологии Jasp рассказывается в лекции 11.

    15.4. Отладка JavaScript

    Теперь, когда наш проект содержит большое количество JavaScript функций, перед разработчиком встает проблема отладки этих функций. Как для отладки HTML и CSS, так и для сценариев можно воспользоваться IE Developer Tools. Для запуска отладчика нужно перейти во вкладку "Сценарий" (Script) и нажать на кнопку Start Debugging. В результате произойдет перезагрузка текущей страницы в открытой вкладке (если в настройках браузера запрещена отладка скриптов), а Developer Tools "открепятся" от нижней части экрана (рис. 15.7). Остановка процесса отладки производится по нажатию на клавиатурное сочетание Shift+F5 или нажав на кнопку Stop Debugging.

    (рис 15.7) Отладка в Developer Tools

    Консоль JavaScript -кода может отслеживать вызов тех или иных переменных, выражений и стеков вызовов

    В ходе процесса возможна установка контрольных точек автоматической остановки выполнения – для этого нужно выделить нужное место в коде скрипта, после чего нажать на кнопку F9. Есть также и запуск отладчика с пропуском ошибок исполнения (во избежание остановок) – для этого можно или щелкнуть по кнопке "Остановка при ошибке" или же продублировать это действие командой Ctrl+Shift+E. Кроме того, в случае, если установленная контрольная точка становится неактуальной (код на странице поменялся, указан неправильный путь до сценария и так далее), то в отладчике можно задать условия, при которых будет выполняться остановка (устанавливается в свойствах точки остановки). Наконец, в случае, когда выполнение сценария приостановлено, пользователь может вручную продолжить работу отладчика (кнопка F5), остановить процесс (Ctrl+Shift+B), перейти на следующий этап (F11) или предыдущий шаг (F10), повторить последний цикл скрипта до текущего состояния (Shift+F11).

    Любая ошибка во время выполнения приводит к тому, что отладчик автоматически останавливается в месте возникновения ошибки. Windows Internet Explorer остановится и выделит строку, вызвавшую ошибку, в окне просмотра исходного кода.

    Примечание. Пока Windows Internet Explorer ожидает ввода от отладчика сценариев, он не отвечает ни на какие воздействия пользователя.

    Выбрать место прерывания выполнения можно при помощи установки точки останова. При наличии точки останова исполнение сценария приостанавливается на строчке, находящейся непосредственно перед точкой останова. В окне просмотра исходного кода подсвечивается следующая исполняемая строчка. Точки останова можно устанавливать и после того, как запущен отладчик. Для этого нужно или щелкнуть рядом с номером строчки кода, или вызвать контекстное меню, или нажать клавишу F9.

    На вкладке Breakpoints изображен список всех доступных точек останова. На рис. 15.8 показана вкладка Breakpoints, выбранная в правой части панели отладчика.

    (рис 15.8) Вкладка "Точки останова" в Developer Tools

    Все точки останова вместе с именами файлов и номерами строк представлены здесь. Дважды щелкнув точку останова в этом списке, можно перейти к исходному коду, в котором она установлена. Сняв флажок рядом с точкой останова, ее можно временно деактивировать, не удаляя из исходного кода. Чтобы удалить точку останова, щелкните ее правой кнопкой мыши и выберите Delete (Удалить).

    Примечание. Даже если перейти со страницы текущего веб-узла, Windows Internet Explorer сохранит информацию о точках останова до тех пор, пока не будут закрыты средства разработчика.

    В любой момент, когда исполнение сценария приостановлено точкой останова, можно просмотреть значения переменных. С помощью средства Locals отладчика можно вывести имена, значения и типы всех переменных, существующих в текущей области видимости сценария. Значения переменных вне области видимости не определены. Область видимости – это интервал, в котором можно обращаться к некоторой переменной. На рис. 15.9 показан вид средства Locals во время отладки.

    (рис 15.9) Средства "Локальные" в Developer Tools

    С помощью средства Watch можно просматривать переменные из разных областей видимости. Чтобы включить просмотр переменной, нужно найти ее в исходном тексте сценария, щелкнуть правой кнопкой мыши и выбрать Add Watch. Таким образом, будет добавлена точка наблюдения для идентификатора, на котором находился курсор. На рис. 15.10 изображено средство Watch с наблюдаемой переменной.

    (рис 15.10) Средства "Смотреть" в Developer Tools

    Еще один способ добавить точку наблюдения переменной – щелкнуть текст "Click to add…" в окне Watch и ввести имя переменной.

    Заметим, что функция профилирования JavaScript добавляет к функциональности отладчика возможность оптимизации кода. Иными словами, Developer Tools содержат инструмент, позволяющий выявить "узкие места" в веб-приложениях – обычно это наиболее часто используемые скрипты. Для этого вебмастеру необходимо инициировать процесс профилирования, чтобы увидеть скорость обработки сценариев. Результат будет представлен в виде списка последовательных вызовов скрипта или отдельных его функций. Чтобы увидеть URL, который относится к данному фрагменту и строку на странице, в IE8 должен быть включен отладчик сценариев.

    15.5. Профилирование JavaScript

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

    Режим Profiler позволяет собрать информацию о временных характеристиках работы веб-узла, которая собирается по мере визуализации страниц веб-узла обозревателем Windows Internet Explorer. Данная информация помогает оптимизировать участки кода, на исполнение которых уходит слишком много времени, то есть узкие места.

    Чтобы приступить к профилированию, необходимо открыть вкладку "Профили" и начать сеанс профилирования сценария нажатием кнопки "Запуск создания профилей". При этом обработчик сценариев переключается в режим профилирования, и на кнопке появляется текст "Остановка создания профилей". Выполнив сценарий, который необходимо профилировать на веб-странице, после чего щелкните "Остановка создания профилей" для завершения сеанса. Автоматически отображается отчет о только что созданном профиле.

    На рис. 15.11 показан основной пользовательский интерфейс вкладки "Профили".

    (рис 15.11) Вкладка "Профили" в Developer Tools

    Отчеты о профилях можно просматривать в представлении "Функции" или "Дерево вызовов", которое можно выбрать из раскрывающегося списка "Текущее представление".

  • В представлении "Функции" перечисляются все используемые функции.
  • В представлении "Дерево вызовов" показана иерархия вызовов.
  • В отчете показаны функции, использовавшиеся обозревателем Windows Internet Explorer для визуализации URL-адреса. Выводится название функции, число ее вызовов, включительное и исключительно время. Включительно время – это время, затраченное на исполнение самой функции и всех функций, из нее вызванных. Исключительное время – это время, затраченное на исполнение самой функции без учета функций, из нее вызванных.

    С помощью собранной профилировщиком информации легко найти узкие места в коде сайта. Если обнаружить и изменить структуру неэффективного кода или медленных алгоритмов, то можно сократить время, которое обозреватель Windows Internet Explorer тратит на визуализацию веб-страниц.

    Данные профиля можно экспортировать из текущего отчета в CSV-файл.

    15.6. Ключевые термины

    ASP.NET, AJAX, JavaScript, JavaScript-библиотеки, ASP.NET AJAX, Веб-сервис, ExtJS.

    15.7. Краткие итоги

    Данное практическое занятие рассматривает следующие темы:

  • Применение технологии AJAX ;
  • Работа с веб-службами в ASP.NET AJAX ;
  • Вызов веб-служб из клиентского сценария с использованием библиотеки ExtJS.
  • Отладка и профилирование JavaScript в IE Developer Tools.
  • Страницы:

    Цель практического занятия: Целью данного занятия является рассмотрение возможности использования языка JavaScript и технологии AJAX для создания визуальных эффектов, а также доступа к данным и их отображения в Интернет-магазине.

    Файлы к практическому занятию Вы можете скачать здесь.

    15.1. Введение

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

    Серверные функции ASP.NET могут быть дополнены функциональностью клиентских сценариев в веб-страницах ASP.NET. Клиентский сценарий можно использовать для предоставления пользователям более функционального и интерактивного интерфейса. Также с помощью клиентского сценария можно осуществлять асинхронные вызовы к веб-серверу во время выполнения страницы в веб-обозревателе.

    ASP.NET предусматривает два подхода к использованию клиентских возможностей. Первый подход – это использование AJAX -функций платформы ASP.NET. Технология AJAX в ASP.NET включает библиотеку функций клиентских сценариев, привнося многие из преимуществ объектно-ориентированного программирования и моделей ASP.NET в клиентские сценарии. AJAX в ASP.NET также обеспечивает серверную поддержку, что включает серверные веб-элементы управления, автоматически отрисовывающие необходимый для функциональности AJAX клиентский сценарий.

    Второй подход – создание клиентского сценария и самостоятельная интеграция его в веб-страницы ASP.NET. Этим подходом удобно пользоваться, если фрагменты клиентского сценария очень невелики или если требуется интегрировать существующий клиентский сценарий в приложение. Данный подход используется многими серверными веб-элементами управления ASP.NET (например, проверяющими элементами управления) для создания клиентской функциональности.

    15.2. Применение AJAX

    Прежде всего, рассмотрим применение технологии AJAX. Для этого в мастер страниц в самое начало содержимого формы (тег form ) необходимо добавить следующий код:

    <asp:scriptmanager ID="Scriptmanager1" runat="server"></asp:scriptmanager>

    Этот объект будет отвечать за работу JavaScript, в частности за подключение нового скриптового кода, который будет скачиваться при асинхронных запросах.

    Также добавим еще один ContentPlaceHolder, который будем использовать как контейнер для JavaScript функций на различных страницах.

    <asp:ContentPlaceHolder ID="PlaceHolderScripts" runat="server"> 
    </asp:ContentPlaceHolder>

    15.2.1. Подключение клиентских сценариев

    Теперь в мастере страниц подключим известную JavaScript-библиотеку jquery (скачать последнюю версию библиотеки и почитать документацию можно по адресу: http://jquery.com/):

    <script type="text/javascript" src="JS/jquery-1_2_6_min.js"></script>

    Перейдем на главную страницу. Воспользовавшись библиотекой jquery, анимируем основной текст страницы, так чтобы он медленно всплывал, и сделаем так, чтобы надпись "Новинки!" мигала. Для этого добавим в PlaceHolderScripts следующий код:

    <asp:Content ID="ContentScript" ContentPlaceHolderID="PlaceHolderScripts" runat="server">
    
        <script type="text/javascript">
            jQuery(document).ready(function() {
                $('#MainText').toggle(1000);
                $('#News').toggleClass('h3').toggle(1000, function() {
                    animateText();
                });
    
            });
    
            function animateText() {
                $('#News').toggleClass('h3').toggle(1000, function() {
                    animateText();
                });
            }
        </script>. 
    </asp:Content>

    Здесь определены две функции. Первая, анонимная функция, сработает в тот момент, когда страница будет сформирована браузером. Так как изначально основному тексту присвоен CSS-класс text-init, который содержит правило display:none, то при загрузке страницы этот текст не отобразится. $('#MainText') найдет элемент с идентификатором MainText, а метод toggle(1000) изменит видимость этого элемента (в данном случае – покажет его), причем процесс отображения текста будет анимирован и займет 1 секунду.

    Для элемента с идентификатором News, будет поочередно "включаться" и "выключаться" применение стиля h3, а функция toogle будет скрывать и показывать надпись, создавая эффект мигания. Здесь используется перегрузка функции toggle, которая отработав в течение секунды, вызовет функцию animateText. Эта функция, в свою очередь, снова изменит видимость надписи и вызовет себя.

    Результат добавления визуальных эффектов представлен на рис. 15.1.

    (рис 15.1) Главная страница сайта с "всплывающим" текстом

    Теперь перейдем к рассмотрению возможностей технологии AJAX. Основная задача, которую она решает, заключается в асинхронном обмене данными с сервером. Это значит, что действия пользователя не приводят к перезагрузке страницы. На самом деле данные отправляются и получаются с сервера JavaScript -объектом XMLHttpRequest, который поддерживается всеми современными браузерами. При это в технологии AJAX ASP.NET, обращение XMLHttpRequest к серверу называется асинхронным "постбэком" (AsyncPostBack) и так же, как и в случае обычного "постбэка", приводит к восстановлению всей страницы на сервере. Это обеспечивается тем, что содержимое ViewState передается на сервер и при асинхронных запросах.

    Так как данные при использовании AJAX передаются независимо от страницы, возникает задача обработки полученного от сервера ответа и отображения его на странице. В общем случае, она решается также применением языка JavaScript. Разработчик пишет клиентский код, который разбирает полученные данные и на его основе генерирует нужный для отображения HTML. В ASP.NET AJAX предоставляется намного более простой способ решения этой проблемы, который заключается в использовании серверного компонента UpdatePanel.

    15.2.2. Работа с UpdatePanel

    При первом рассмотрении UpdatePanel сходен в работе с обычным Panel. В результате рендеренга страницы этот компонент также заменяется на HTML-элемент div и служит для группировки и разметки страницы. Особенность же этого компонента заключается в том, что любой запрос, совершенный элементом, расположенным внутри UpdatePanel является асинхронным и приводит только к перерисовке его содержимого. Остальная страница не изменяется. Даже если при постбэке пользователь изменит структуру или набор компонент вне UpdatePanel, то на этапе Render жизненного цикла страницы, эти изменения не будут учтены, произойдет генерация HTML-разметки только для содержимого компонента и эта разметка будет отправлена клиенту. Клиентская часть ASP.NET AJAX обработает полученный код и изменить разметку внутри div' а, соответствующего UpdatePanel.

    Таким образом, расположив на странице несколько UpdatePanel' ов (а их можно расположить любое количество, вкладывая, например, один в другой) можно реализовать интерфейс, который никогда не будет перерисовывать страницу и в то же время предоставлять все функциональные возможности сайта.

    Иногда бывает не достаточно обновлять только содержимое того UpdatePanel' а, который вызвал постбэк. Нужно заставить обновиться и другие UpdatePanel' ы на странице. Это можно сделать двумя способами:

  • В разметке ASP или в code-behind UpdatePanel указать триггеры ( Triggers ), которые будут приводить к перерисовке содержимого этой панели. В качестве параметров указываются идентификаторы серверных компонент, находящихся вне UpdatePanel, которые должны привести к обновлению его содержимого.
  • В code-behind'e можно вызвать метод UpdatePanel.Update(), что приведет к тому, что его содержимое будет ренедрится на сервере и отправляться клиенту.
  • Все это значит, что для внедрения AJAX в наш Интернет-магазин достаточно поместить все необходимые компоненты в UpdatePanel' ы и при необходимости указать триггеры:

    <asp:UpdatePanel ID="UpdatePanelComments" 
           runat="server" UpdateMode="Conditional">
      <Triggers>
        <asp:AsyncPostBackTrigger ControlID="AddReview"  /> 
      </Triggers>
      <ContentTemplate>
        <asp:DataList ID="ReviewList" runat="server" Width="100%" 
            CellPadding="0" CellSpacing="0">
          <ItemTemplate>
            <asp:Label ID="Label1" style='font-weight:bold' 
    	      Text='<%# DataBinder.Eval(Container.DataItem, "ReviewerName") %>'
                  runat="server" />
            <span class="Normal">говорит... </span>
            <asp:Image ID="Image1" runat="server" 
    	      ImageUrl='<%# "~/images/ReviewRating" + DataBinder.Eval(Container.DataItem, "Rating") +".gif" %>' />
            <br>
            <asp:Label ID="Label2" CssClass="Normal" 
                 Text='<%# DataBinder.Eval(Container.DataItem, "Comments") %>'
                 runat="server" />
          </ItemTemplate>
          <SeparatorTemplate>
            <br>
          </SeparatorTemplate>
        </asp:DataList>
      </ContentTemplate>
    </asp:UpdatePanel>
    …
    <asp:UpdatePanel ID="UpdatePanelNewCommentProperties" runat="server" 
         UpdateMode="Conditional">
      <Triggers><asp:AsyncPostBackTrigger ControlID="AddReview"  /> 
      </Triggers>
      <ContentTemplate>
        …
      </ContentTemplate>
    </asp:UpdatePanel>
    
    …
    <asp:UpdatePanel ID="UpdatePanelCommentButton" runat="server">
      <ContentTemplate>
        <br>
        <span class="NormalBold">Комментарий</span>
        <br>
        <asp:TextBox ID="Comment" TextMode="multiline" 
            MaxLength="3850" Rows="7" Width="100%"
            runat="server" />
        <asp:RequiredFieldValidator ControlToValidate="Comment" 
            Display="Dynamic" Font-Names="verdana" Font-Size="9pt" 
            ErrorMessage="Поле 'Комментарий' должно быть заполнено." runat="server"
            ID="RequiredFieldValidator3"></asp:RequiredFieldValidator>
        <br>
        <br>
        <asp:Button ID="AddReview" runat="server" 
            OnClick="OnClick" Text="Добавить свой комментарий" />
        <br>
      </ContentTemplate>
    </asp:UpdatePanel>

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

    15.3. Работа с веб-службами в ASP.NET AJAX

    Теперь, когда мы научились работать с языком JavaScript и сумели оптимизировать работу сайта при помощи ASP.NET AJAX стоит оценить преимущества и недостатки текущей реализации Интернет-магазина. Нам удалось сократить время работы серверного кода (хотя и незначительно) за счет того, что мы используем UpdatePanel, и теперь метод Page.Render отрисовывает не всю странице целиком, а только определенные части. Более того, уменьшился и объем данных, передаваемый от сервера клиенту, опять же за счет того, что передается не вся страница. Второе преимущество, которого нам удалось добиться, заключается в том, что пользователь не наблюдает процесс перезагрузки всей страницы при каждой обратной передаче, что делает пользовательский интерфейс страницы более естественным.

    Среди недостатков стоит отметить тот факт, что каждое действие пользователя приводит к отправке на сервер и получение от него большого объема данных в виде ViewState. Если же отказаться от использования ViewState, то для восстановления страницы на сервере необходимо либо хранить все данные в сессии, либо каждый раз извлекать их из базы данных. Еще один недостаток заключается в том, что при использовании достаточно сложного интерфейса, насыщенного большим количеством сложных компонент, время их рендеринга на сервере сильно возрастает.

    Один из подходов к решению этих проблем заключается в использовании веб-сервисов. Вместо того чтобы каждый раз ради отправки или получения данных посылать или получать всю страницу целиком, можно сделать так, чтобы клиентский код JavaScript обращался на сервер, получал необходимые данные и сам же их отображал. Этот подход серьезно сократит трафик между сервером и клиентом (в случае с таблицей продуктов, о которой в дальнейшем пойдет речь, вместо 17-18 килобайт данных будет передаваться 1 килобайт) и снизит нагрузку на сервер, так как теперь его основные обязанностями будут заключаться в том, чтобы манипулировать данными.

    При этом, однако, многократно возрастет нагрузка на клиент. Это может привести к тому, что данный подход станет неприменим в случаях, когда у клиентов слабые компьютеры или на них установлены старые браузеры, плохо поддерживающие JavaScript.

    Тем не менее, для сайта, с которыми работает одновременно множество людей, этот подход способен серьезно улучшить производительность приложения.

    15.3.1. Создание веб-службы

    Для того чтобы добавить веб-сервис в проект, необходимо кликнуть правой кнопкой мыши по проекту в окне Solution Explorer и выбрать раздел меню Add New Item. В открывшемся диалоговом окне, необходимо выбрать элемент Web Service (рис. 15.2).

    (рис 15.2) Окно добавления нового веб-сервиса в проект

    В результате в проект будет добавлено два файла: WebProductService.asmx и WebProductService.cs, причем последний фал будет помещен в директорию App_code.

    Чтобы разрешить вызов веб-служб (ASMX) из клиентского сценария на веб-странице ASP.NET, необходимо добавить на страницу элемент управления ScriptManager. Чтобы определить ссылку на веб-службу, необходимо добавить дочерний элемент asp:ServiceReference к элементу управления ScriptManager. После этого необходимо установить URL-адрес веб-службы в качестве значения атрибута ссылки на сервер path. Объект ServiceReference определяет необходимость создания прокси-класса JavaScript для вызова указанной веб-службы в ASP.NET.

    Так как в нашем случае ScriptManager определен на мастере страниц, изменим его код следующим образом:

    <asp:ScriptManager ID="Scriptmanager1" runat="server">
                <Services>
                    <asp:ServiceReference Path="~/WebProductService.asmx" />
                </Services>
    </asp:ScriptManager>

    Теперь необходимо реализовать код этого сервиса. Для примера сделаем так, чтобы сервис мог возвращать список продуктов с учетом указанной категории и подкатегории и при этом поддерживал постраничный вывод.

    Прежде чем приступить к реализации самого сервиса, напишем два вспомогательных класса. Первый класс ProductDTO (DTOData Transfer Object) будет представлять собой описание структуры данных продукта, которую сервис будет отправлять клиенту.

    [Serializable]
    public class ProductDTO
    {
        public int ProductID { get; set; }
        public string ProductNumber { get; set; }
        public string Name { get; set; }
        public string Color { get; set; }
        public decimal ListPrice { get; set; }
        public string FullSize { get; set; }
        public string Weight { get; set; }
        
        public ProductDTO(Product p)
        {
            ProductID = p.ProductID;
            ProductNumber = p.ProductNumber;
            Name = p.Name;
            Color = p.Color;
            ListPrice = p.ListPrice;
            FullSize = p.FullSize;
            Weight = p.Weight + p.WeightUnitMeasureCode;
        }
    
        public ProductDTO()
        {
            
        }
    }
    Примечание 1: Атрибут [Serializable] в данном случае необязателен. Он показывает, что объекты этого классы должны поддерживать возможность представляться в виде строки, которую можно передать от одного сервиса к другому так, чтобы получающий сервис смог восстановить передаваемый объект. Сериализовать объект можно в xml -файл, json -объект или в бинарный код. Примечание 2: так как данный класс сериализуем, необходимо чтобы у него был определен публичный конструктор без параметров. Примечание 3: нам необходимо разработать вспомогательные классы, так как класс Product не сериализуем. Это связано с тем, что сериализации подвергаются все public свойства, среди которых есть ссылки на объекты, которые также рекурсивно сериализуются. Так, например, Product имеет ссылку ProductSubcategory на свою подкатегорию, которая в свою очередь имеет ссылку Products. В результате при сериализации произойдет зацикливание.

    Второй класс также будет использоваться для передачи информации клиенту. Он состоит из двух свойств:

  • Result предназначается для передачи коллекции ProductDTO, которая удовлетворяет текущим критериям поиска;
  • TotalCount содержит общее количество продуктов, которые соответствуют критериям поиска.
  • Этот класс позволит организовать постраничный вывод продуктов. Ниже представлен код этого класса:

    public class ResultStructure
    {
        public object Result{get;set;}
        public int TotalCount { get; set; }
    }

    Теперь, когда все вспомогательные структуры готовы, необходимо определить сервис, который будет обрабатывать запросы:

    /// <summary>
    /// Summary description for WebProductService
    /// </summary>
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    // To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line. 
    [System.Web.Script.Services.ScriptService]
    public class WebProductService : System.Web.Services.WebService {

    Этот код будет автоматически добавлен Visual Studio, при создании сервиса. Нам необходимо разкомментировать атрибут ScriptService, так как он позволяет вызвать сервис из JavaScript.

    Дальше в самом классе реализуются доступные для вызова веб-методы. Все такие методы помечаются атрибутом WebMethod. В нашем примере мы разработаем только один метод GetProducts, который на вход будет получать текущую категорию, подкатегорию и номер страницы:

    [WebMethod]
    [ScriptMethod(ResponseFormat = ResponseFormat.Json)]
    public ResultStructure GetProducts(string category, string subcategory, int skip)
    {
      DataClassesDataContext dcdc =
        new DataClassesDataContext(
            "Data Source=localhost;Initial Catalog=AdventureWorks;Integrated Security=True");
      var query = from p in dcdc.Products select p;
      if (!string.IsNullOrEmpty(subcategory))
      {
        query = query.Where(p => p.ProductSubcategoryID == Convert.ToInt32(subcategory));
          
      }
      else
      {
        if (!string.IsNullOrEmpty(category))
        {
          query = query.Where(p => p.ProductSubcategory.ProductCategoryID == Convert.ToInt32(category));
        }
      }
      return new ResultStructure()
      {
        Result = query.Skip(skip).Take(10).Select(p => new ProductDTO(p)).ToList(),
        TotalCount = query.Count()
      };
    }

    Код веб-метода идентичен коду, который разрабатывался на предыдущих занятиях для заполнения GridView данными о продуктах, только теперь метод не привязывает данные к какому-либо серверному компоненту, а возвращает сериализованные данные состоящие из коллекции продуктов и их общего количества. В атрибуте ScriptMethod указан параметр ResponseFormat = ResponseFormat.Json, который указывает, что результат необходимо сериализовать в формате JSON. Мы выбрали этот формат, так как он является основным формат представления данных для языка JavaScript.

    Если вызвать наш сервис и передать ему параметры, то будет получен ответ, аналогичный приведенному:

    {"d":{"__type":"ResultStructure",
            "Result":
                   [{"ProductID":982,"ProductNumber":"BK-M38S-42",
    	             "Name":"Mountain-400-W Silver, 42",
    			     "Color":"Silver","ListPrice":769.4900,
    			     "FullSize":"42 CM ","Weight":"27,13LB "},
    			    {"ProductID":983,"ProductNumber":"BK-M38S-46",
    			     "Name":"Mountain-400-W Silver, 46",
    			     "Color":"Silver","ListPrice":769.4900,
    			     "FullSize":"46 CM ","Weight":"27,42LB "},
    			    {"ProductID":984,"ProductNumber":"BK-M18S-40",
    			     "Name":"Mountain-500 Silver, 40",
    			     "Color":"Silver","ListPrice":564.9900,
    			     "FullSize":"40 CM ","Weight":"27,35LB"},
    			    {"ProductID":985,"ProductNumber":"BK-M18S-42",
    			     "Name":"Mountain-500 Silver, 42",
    			     "Color":"Silver","ListPrice":564.9900,
    			     "FullSize":"42 CM ","Weight":"27,77LB "},
    			    {"ProductID":986,"ProductNumber":"BK-M18S-44",
    			     "Name":"Mountain-500 Silver, 44",
    			     "Color":"Silver","ListPrice":564.9900,
    			     "FullSize":"44 CM ","Weight":"28,13LB "},
    			    {"ProductID":987,"ProductNumber":"BK-M18S-48",
    			     "Name":"Mountain-500 Silver, 48",
    			     "Color":"Silver","ListPrice":564.9900,
    			     "FullSize":"48 CM ","Weight":"28,42LB "},
    			    {"ProductID":988,"ProductNumber":"BK-M18S-52",
    			     "Name":"Mountain-500 Silver,52",
    			     "Color":"Silver","ListPrice":564.9900,
    			     "FullSize":"52 CM ","Weight":"28,68LB "},
    			    {"ProductID":989,"ProductNumber":"BK-M18B-40",
    			     "Name":"Mountain-500 Black, 40",
    			     "Color":"Black","ListPrice":539.9900,
    			     "FullSize":"40 CM ","Weight":"27,35LB "},
    			    {"ProductID":990,"ProductNumber":"BK-M18B-42",
    			     "Name":"Mountain-500 Black, 42",
    			     "Color":"Black","ListPrice":539.9900,
    			     "FullSize":"42 CM ","Weight":"27,77LB "},
    			    {"ProductID":991,"ProductNumber":"BK-M18B-44",
    			     "Name":"Mountain-500 Black, 44",
    			     "Color":"Black","ListPrice":539.9900,
    			     "FullSize":"44 CM ","Weight":"28,13LB "}],
            "TotalCount":32}}

    При этом для сервиса будут доступны страницы, на которых будут представлены описание методов, которые предоставляет сервис, а также тестовые страницы, на которых можно вызвать метод, передав параметры. Впрочем, в случае использования JSON-формата данных, вызвать методы не получится, так как это приведет к ошибке. Примеры таких страниц представлены на рис. 15.3 и рис. 15.4.

    (рис 15.3) Страница с описанием сервиса(рис 15.4) Тестовая страница для операции GetProduct сервиса WebProductService

    15.3.2. Вызов веб-службы из клиенсткого сценария

    Теперь, когда веб-сервис реализован, можно переделать страницу products/default.aspx, сделав так, чтобы она отображала данные, которые будет возвращать веб-сервис. Чтобы облегчить работу с отображением данных, воспользуемся библиотекой ExtJS, ознакомиться и скачать которую можно по адресу: http://www.extjs.com/

    Примечание: библиотека ExtJS является коммерческим продуктом. Узнать о правилах лицензирования можно по адресу: http://www.extjs.com/products/license.php

    Подключим библиотеку ExtJS к нашему проекту, добавив ссылки на файлы ext-all.css, ext-base.js и ext-all.js в head' е мастера страниц так, как это указано в следующем коде:

    <head runat="server">
      <meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
      <title></title>
      <link href="styles/style2.css" media="screen" rel="stylesheet" title="CSS" type="text/css" />
      <link rel='stylesheet' type='text/css' href='styles/ext-all.css' />
    
      <script type="text/javascript" src="../JS/ext-base.js" ></script>
    
      <script type="text/javascript" src="../JS/ext-all.js" ></script>
    
      <script type="text/javascript" src="JS/jquery-1_2_6_min.js"></script>
    
    </head>

    Теперь перейдем к странице products/default.aspx, удалим код, содержащийся в ContentPlaceHolder' ах и серверный код.

    Теперь в левый ContentPlaceHolder добавим тег script, где разместим следующий код:

    Ext.onReady(function(){
       var query = getQueryVariable();
       var result = WebProductService.GetProducts
        (query.category ? query.category : '', 
         query.subCategory ? query.subCategory : '',0, SucceededCallback, FailedCallback);
      });
      function FailedCallback(error) {
        alert(error.message);
      }
      function SucceededCallback(result, eventArgs) {
      }
      function getQueryVariable() {
        //полачаем строку запроса (?a=123b=qwe) и удаляем знак ?
        var query = window.location.search.substring(1);
        //получаем массив значений из строки запроса вида vars[0] = 'a=123';
        var vars = query.split("");
        var arr = new Array();
        //переводим массив vars в обычный ассоциативный массив 
        for (var i = 0; i < vars.length; i++) {
          var pair = vars[i].split("=");
          arr[pair[0]] = pair[1];
        }
        return arr;
    }

    Функцию SucceededCallback на данный момент определять не будем, так как прежде нам потребуется написать вспомогательный код.

    В приведенном коде определены 4 функции. Первая функция является анонимной, и выполняется в тот момент, когда библиотека ExtJS закончит инициализацию, что произойдет после полной отрисовки страницы. Вначале будет вызвана вспомогательная функция getQueryVariable, которая позволит определить, какие переменные определены в строке запроса.

    Функция getQueryVariable обращается к объекту window.location, который содержит всю строку запроса, после чего обращается в свойству search, содержащему строку параметров в формате ?a=123b=qwe. Далее у этой строки удаляется первый символ, и она методом split разделяется на множество строк, разделенных знаком . После этого, каждая полученная строка также разделяется на две, содержащие имя параметра и его значение, и записываются в массив arr, который в данном случае выступает в качестве хэш-таблицы.

    Результат вызова функции getQueryVariable помещается в переменную query.

    Далее вызывается функция WebProductService.GetProducts, которая представляет собой сгенерированную ScriptManager' ом прокси-функцию, вызывающую одноименный веб-метод. При этом JavaScript устроен так, что вызов самого веб-метода будет отложен до тех пор, пока текущий поток не закончит свою работу. Чтобы узнать, как отработал веб-метод, в функцию WebProductService.GetProducts помимо трех параметров, необходимых для работы, передаются также два метода обратного вызова. Метод FailedCallback будет вызван в случае, если во время выполнения веб-метода произойдет ошибка, и отобразит текст этой ошибки пользователю. Метод SucceededCallback выполнится в случае, если веб-метод успешно завершит свое выполнение. В этом случае переменная result будет содержать результат работы веб-метода.

    Уже сейчас можно запустить наш сайт и, если открыть IE Developer Tools и поставить breakpoint в метод SucceededCallback, можно убедиться, что веб-сервис возвращает нужный ответ, изучив переменную result (рис. 15.5).

    (рис 15.5) Переменная result в IE Developer Tools

    Однако данные будут бесполезны, если пользователь их не увидит. Для того чтобы отобразить данные, воспользуемся клиентским компонентом Ext.grid.GridPanel, который предоставляет во многом схожую функциональность с той, что дает ASP.NET -компонент GridView, но позволяет выполнять большинство действий без взаимодействия с сервером, не перерисовывая страницу.

    Как и для ASP.NET GridView необходимо для Ext.grid.GridPanel указать источник данных, который должен наследоваться от класса Ext.data.Store. Так как разработанный веб-метод возвращает данные в формате JSON, то нам подойдет класс Ext.data.JsonStore. Каждый источник данных в этом подходе состоит из двух важных компонент: DataProxy, который отвечает за извлечение данных, и Reader' а, который умеет полученные при помощи DataProxy данные разбирать. И если стандартный класс JsonReader нас устроит для разбора данный, то ни одна из реализаций DataProxy не позволяет обращаться к веб-сервисам. Поэтому мы разработаем собственный (сильно упрощенный) прокси-класс, который назовем Ext.data.FunctionProxy:

    Ext.data.FunctionProxy = function(f) {
       Ext.data.FunctionProxy.superclass.constructor.call(this);
       this.func = f;
    };
    
    Ext.extend(Ext.data.FunctionProxy, Ext.data.DataProxy, {
       load: function(params, reader, loadCallback, scope, arg) {
       var proxy = this;
       this.func({
          first: params.start,
          count: params.limit,
          sortAsc: params.dir != "DESC",
          scope: params.scope,
          totalCount: proxy.totalCount || 0,
          fields: params.fields
       },
       function(data) {
          var records = reader.readRecords(data);
          proxy.totalCount = records.totalRecords;
          proxy.lastArguments = proxy.arguments;
          loadCallback.call(scope, records, arg, true);
       });
     },
      reload: function() {
       this.load.apply(this, this.lastArguments);
      }
    })

    При разработке этого класса используется объектно-ориентированный подход программирования на JavaScript, предлагаемый библиотекой ExtJS. Вначале объявляется конструктор класса Ext.data.FunctionProxy, который принимает на вход переменную-функцию f и который вызывает конструктор базового класса. При этом переменная f сохраняется в поле func текущего объекта.

    Далее определяется, что класс Ext.data.FunctionProxy является расширением класса Ext.data.DataProxy, у которого переопределены методы load и reload. Это делается при помощи статического метода Ext.extend.

    Примечание: использование методов Ext в данном случае позволяет создать подход к программированию, соответствующий традициям ООП. Те же действия можно выполнить, не привлекая объектно-ориентированный код стандартными возможностями JavaScript, однако в этом случае сильно упадет наглядность кода.

    Функция load вызывает функцию func, передавая параметры в виде объекта arg, поля которого содежат информацию о номере первой извлекаемой запись, количестве записей, направлении сортировки, области видимости, полном количестве записей и наборе извлекаемых полей. Второй передаваемый в функцию func параметр представляет собой функцию обратного вызова, которая сработает, когда данные с сервера будут извлечены (т.е. заменит функцию SucceededCallback ). Эта функция применяет JsonReader для разбора записей и представления их в виде, понятном различным компонентам, включая Ext.grid.GridPanel, а также сохраняет информацию об общем количестве записей и аргументах вызова. Далее вызывается функция loadCallback, которая и сообщит связанным с этим источником данных компонентам, что в него загружены новые данные.

    Функция reload просто повторно вызывает функцию load с последними использованными параметрами.

    Последнее, что осталось подготовить – это место, где будет отрисовываться таблица. Для этого добавим ASP-панель на страницу products/default.aspx:

    <asp:Panel ID="PanelGrig" runat="server" Style="width:100%;" >
    
    </asp:Panel>

    Теперь приступим к реализации функции SucceededCallback. Для простоты, разобьем ее код на несколько частей:

    var storeProducts = new Ext.data.JsonStore({
        root: 'Result', remoteSort: true,
        fields: ['ProductID', 'ProductNumber', 'Name', 
                 'Color', 'ListPrice', 'FullSize', 'Weight'],
        totalProperty: 'TotalCount',
        proxy: new Ext.data.FunctionProxy(function(arg, loadCallback)
        {
          var query = getQueryVariable();
          var query = getQueryVariable();
          WebProductService.GetProducts(query.category ? query.category : '', 
                                        query.subCategory ? query.subCategory : '', 
                                        arg.first, loadCallback);
        })
    });
    storeProducts.load({
       params: {
          start: 0,
          limit: 10
       }
    });

    Здесь мы определяем источник данных storeProducts типа Ext.data.JsonStore, который будем использовать в дальнейшем. При этом указывается, что записи будут храниться в поле Result, а общее количество записей в поле TotalCount возвращаемого веб-сервисом JSON-объекта. Указывается, что сортировка записей проводится на сервере, определяется набор полей, которые будут храниться в источнике данных, и указывается прокси-класс. Здесь мы воспользуемся новым классом Ext.data.FunctionProxy, а передаваемая в него функция, повторяет код анонимной функции, рассмотренной ранее. После этого вызывается функция load источника данных, хотя это не обязательно.

    Далее определяется div, в который будет отрисована таблица. Здесь стоит обратить внимание, на то, что в качестве идентификатор при поиске элемента на странице используется не ID ASP-панели, а ее ClientId, которое генерируется ASP.NET автоматически. Данный подход, когда идентификатор "зашит" в код не очень хорош, так как идентификатор может поменяться, если будет изменена сама страница ASP. Но так как у нас всего один ASP-компонент на странице, то этим можно пренебречь:

    var el = 
    document.getElementById('ctl00_column_l_placeholder_PanelGrig');
    Теперь осталось отобразить саму таблицу:
                var grid = new Ext.grid.GridPanel({
                    renderTo: el,
                    stripeRows: true,
                    store: storeProducts,
                    colModel: new Ext.grid.ColumnModel({
                        defaults: {
                            width: 120,
                            sortable: true
                        },
                        columns: [
                        { header: 'Номер продукта', dataIndex: 'ProductNumber' },
                        { header: 'Название', dataIndex: 'Name' },
                        { header: 'Цвет', dataIndex: 'Color' },
                        { header: 'Цена', dataIndex: 'ListPrice' },
                        { header: 'Размер', dataIndex: 'FullSize' },
                        { header: 'Вес', dataIndex: 'Weight' }
                
                    ]
                    }),
                    viewConfig: {
                        forceFit: true
                    },
                    sm: new Ext.grid.RowSelectionModel({ singleSelect: true }),
                    width: 600,
                    height: 300,
                    frame: true,
             bbar: new Ext.PagingToolbar(
                {
                    pageSize: 10,
                    store: storeProducts,
                    listeners:
                    {
                        beforechange: function(sender, e) {
                        sender.store.reload(
                                {
                                    params:
                                    {
                                        start: e.start,
                                        limit: e.limit,
                                        scope: this
                                    }
                                });
                            return false;
                        }
                    }
                })
                });
            }

    Отметим только основные моменты этого кода. Компоненту указывается источник данных storeProducts, а также элемент el, в который этот компонент должен быть отрисован. Также указана модель столбцов, в которой определены все колонки, их заголовки и связь с полем источника данных. Также для нижней панели таблицы ( bbar ) определен компонент Ext.PagingToolbar, который обеспечивает возможность переключения страниц. Для этого в параметрах конструктора указан тот же источник данных, что и у таблицы, определен размер страницы, а в поле listeners определен обработчик клиентского события beforechange, в котором у источника данных вызывается метод reload, но уже для новой страницы (указывается новый стартовый элемент). Строчка return false ; позволяет избежать вызова других обработчиков, прикрепленных к событию beforechange пэйджера.

    Теперь все готово, и мы можем запустить наше веб-приложение и убедиться, что все работает. Примеры получившихся страниц представлены на рис. 15.6, рис. 15.7 и рис. 15.8.

    (рис 15.6) Первая страница списка всех продуктов(рис 15.7) 34-ая страница списка всех продуктов(рис 15.8) Список всех горных велосипедов

    Рассмотренный подход, обладает множеством преимуществ, однако также необходимо указать и недостатки такого подхода. Прежде всего, отметим, что теряется возможность визуально верстать страницу, так как весь код генерируется JavaScript (некоторые работы в решении этой проблемы ведутся, например: http://www.extjs.com/products/designer/). Разработка клиентских скриптов достаточно слабо поддерживается в Microsoft Visual Studio 2008. В этой области некоторые улучшения ожидаются от новой среды разработки Microsoft Visual Studio 2010. Еще одна проблема – это плохая интеграция ASP.NET и клиентских визуальных компонент. Здесь также есть наработки, в частности проект JASP (http://jaspproject.ru и http://code.google.com/p/jaspproject/) предлагает расширение для Visual Studio 2008, которое позволит разрабатывать клиентские компоненты в стиле ASP.NET. Это позволяет с одной стороны полностью отказаться от перезагрузки страниц, и использовать исключительно веб-сервисы как средство взаимодействие с сервером, а с другой – интегрировать веб-методы напрямую в компоненты и вызвать их из JavaScript кода как обычные скриптовые функции, что свойственно серверным компонентам. Подробнее о технологии Jasp рассказывается в лекции 11.

    15.4. Отладка JavaScript

    Теперь, когда наш проект содержит большое количество JavaScript функций, перед разработчиком встает проблема отладки этих функций. Как для отладки HTML и CSS, так и для сценариев можно воспользоваться IE Developer Tools. Для запуска отладчика нужно перейти во вкладку "Сценарий" (Script) и нажать на кнопку Start Debugging. В результате произойдет перезагрузка текущей страницы в открытой вкладке (если в настройках браузера запрещена отладка скриптов), а Developer Tools "открепятся" от нижней части экрана (рис. 15.7). Остановка процесса отладки производится по нажатию на клавиатурное сочетание Shift+F5 или нажав на кнопку Stop Debugging.

    (рис 15.7) Отладка в Developer Tools

    Консоль JavaScript -кода может отслеживать вызов тех или иных переменных, выражений и стеков вызовов

    В ходе процесса возможна установка контрольных точек автоматической остановки выполнения – для этого нужно выделить нужное место в коде скрипта, после чего нажать на кнопку F9. Есть также и запуск отладчика с пропуском ошибок исполнения (во избежание остановок) – для этого можно или щелкнуть по кнопке "Остановка при ошибке" или же продублировать это действие командой Ctrl+Shift+E. Кроме того, в случае, если установленная контрольная точка становится неактуальной (код на странице поменялся, указан неправильный путь до сценария и так далее), то в отладчике можно задать условия, при которых будет выполняться остановка (устанавливается в свойствах точки остановки). Наконец, в случае, когда выполнение сценария приостановлено, пользователь может вручную продолжить работу отладчика (кнопка F5), остановить процесс (Ctrl+Shift+B), перейти на следующий этап (F11) или предыдущий шаг (F10), повторить последний цикл скрипта до текущего состояния (Shift+F11).

    Любая ошибка во время выполнения приводит к тому, что отладчик автоматически останавливается в месте возникновения ошибки. Windows Internet Explorer остановится и выделит строку, вызвавшую ошибку, в окне просмотра исходного кода.

    Примечание. Пока Windows Internet Explorer ожидает ввода от отладчика сценариев, он не отвечает ни на какие воздействия пользователя.

    Выбрать место прерывания выполнения можно при помощи установки точки останова. При наличии точки останова исполнение сценария приостанавливается на строчке, находящейся непосредственно перед точкой останова. В окне просмотра исходного кода подсвечивается следующая исполняемая строчка. Точки останова можно устанавливать и после того, как запущен отладчик. Для этого нужно или щелкнуть рядом с номером строчки кода, или вызвать контекстное меню, или нажать клавишу F9.

    На вкладке Breakpoints изображен список всех доступных точек останова. На рис. 15.8 показана вкладка Breakpoints, выбранная в правой части панели отладчика.

    (рис 15.8) Вкладка "Точки останова" в Developer Tools

    Все точки останова вместе с именами файлов и номерами строк представлены здесь. Дважды щелкнув точку останова в этом списке, можно перейти к исходному коду, в котором она установлена. Сняв флажок рядом с точкой останова, ее можно временно деактивировать, не удаляя из исходного кода. Чтобы удалить точку останова, щелкните ее правой кнопкой мыши и выберите Delete (Удалить).

    Примечание. Даже если перейти со страницы текущего веб-узла, Windows Internet Explorer сохранит информацию о точках останова до тех пор, пока не будут закрыты средства разработчика.

    В любой момент, когда исполнение сценария приостановлено точкой останова, можно просмотреть значения переменных. С помощью средства Locals отладчика можно вывести имена, значения и типы всех переменных, существующих в текущей области видимости сценария. Значения переменных вне области видимости не определены. Область видимости – это интервал, в котором можно обращаться к некоторой переменной. На рис. 15.9 показан вид средства Locals во время отладки.

    (рис 15.9) Средства "Локальные" в Developer Tools

    С помощью средства Watch можно просматривать переменные из разных областей видимости. Чтобы включить просмотр переменной, нужно найти ее в исходном тексте сценария, щелкнуть правой кнопкой мыши и выбрать Add Watch. Таким образом, будет добавлена точка наблюдения для идентификатора, на котором находился курсор. На рис. 15.10 изображено средство Watch с наблюдаемой переменной.

    (рис 15.10) Средства "Смотреть" в Developer Tools

    Еще один способ добавить точку наблюдения переменной – щелкнуть текст "Click to add…" в окне Watch и ввести имя переменной.

    Заметим, что функция профилирования JavaScript добавляет к функциональности отладчика возможность оптимизации кода. Иными словами, Developer Tools содержат инструмент, позволяющий выявить "узкие места" в веб-приложениях – обычно это наиболее часто используемые скрипты. Для этого вебмастеру необходимо инициировать процесс профилирования, чтобы увидеть скорость обработки сценариев. Результат будет представлен в виде списка последовательных вызовов скрипта или отдельных его функций. Чтобы увидеть URL, который относится к данному фрагменту и строку на странице, в IE8 должен быть включен отладчик сценариев.

    15.5. Профилирование JavaScript

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

    Режим Profiler позволяет собрать информацию о временных характеристиках работы веб-узла, которая собирается по мере визуализации страниц веб-узла обозревателем Windows Internet Explorer. Данная информация помогает оптимизировать участки кода, на исполнение которых уходит слишком много времени, то есть узкие места.

    Чтобы приступить к профилированию, необходимо открыть вкладку "Профили" и начать сеанс профилирования сценария нажатием кнопки "Запуск создания профилей". При этом обработчик сценариев переключается в режим профилирования, и на кнопке появляется текст "Остановка создания профилей". Выполнив сценарий, который необходимо профилировать на веб-странице, после чего щелкните "Остановка создания профилей" для завершения сеанса. Автоматически отображается отчет о только что созданном профиле.

    На рис. 15.11 показан основной пользовательский интерфейс вкладки "Профили".

    (рис 15.11) Вкладка "Профили" в Developer Tools

    Отчеты о профилях можно просматривать в представлении "Функции" или "Дерево вызовов", которое можно выбрать из раскрывающегося списка "Текущее представление".

  • В представлении "Функции" перечисляются все используемые функции.
  • В представлении "Дерево вызовов" показана иерархия вызовов.
  • В отчете показаны функции, использовавшиеся обозревателем Windows Internet Explorer для визуализации URL-адреса. Выводится название функции, число ее вызовов, включительное и исключительно время. Включительно время – это время, затраченное на исполнение самой функции и всех функций, из нее вызванных. Исключительное время – это время, затраченное на исполнение самой функции без учета функций, из нее вызванных.

    С помощью собранной профилировщиком информации легко найти узкие места в коде сайта. Если обнаружить и изменить структуру неэффективного кода или медленных алгоритмов, то можно сократить время, которое обозреватель Windows Internet Explorer тратит на визуализацию веб-страниц.

    Данные профиля можно экспортировать из текущего отчета в CSV-файл.

    15.6. Ключевые термины

    ASP.NET, AJAX, JavaScript, JavaScript-библиотеки, ASP.NET AJAX, Веб-сервис, ExtJS.

    15.7. Краткие итоги

    Данное практическое занятие рассматривает следующие темы:

  • Применение технологии AJAX ;
  • Работа с веб-службами в ASP.NET AJAX ;
  • Вызов веб-служб из клиентского сценария с использованием библиотеки ExtJS.
  • Отладка и профилирование JavaScript в IE Developer Tools.
  • Вернуться к учебному плану