AJAX при помощи DataContractJsonSerializer и ashx-handler

Вячеслав Гринин, November 2, 2010

В продолжение разговора про AJAX хочу написать несколько статей, описывающих создание AJAX-приложений с применением различных подходов. А подходы эти в принципе могут отличаться в следующих ключевых моментах:

  1. Тип клиентской JS-библиотеки
  2. Тип серверного ответчика
  3. Способ сериализации данных
  4. Тип передаваемых данных

Как видим, здесь есть где развернуться. Расскажу подробнее о каждом из пунктов.

1. Тип клиентской JS-библиотеки.

Это тот скрипт, который работает в браузере пользователя. Какую бы библиотеку Вы не использовали, в недрах ее для асинхронного обмена данными в любом случае будет использоваться объект XmlHTTPRequest. Я лично пользовался такими библиотеками как: ExtJS, jQuery, prototype.js, ATLAS. Сложно сравнивать эти библиотеки, все они хороши, каждая по своему.

2. Тип серверного ответчика.

Прежде всего серверный ответчик характеризует язык серверной реализации, это может быть PHP, ASP.NET, Java, Ruby, Python и много других страшных слов. :) В каждом языке есть свои типы серверных ответчиков. Говоря про ASP.NET, я могу выделить WebServices (*.asmx), Generic Handlers (*.ashx).

3. Способ сериализации данных.

Их можно сериализовать “вручную”, то есть простой конкатенацией строк, можно использовать встроенные в серверную платформу объекты, например DataContractJsonSerializer, который имеется в ASP.NET.

4. Тип передаваемых данных.

Данные можно передавать в собственном формате, можно в виде JSON-строки, XML-строки, или же передавая строку с готовой HTML-разметкой. Вариантов масса, выбор зависит от контекста задачи.

В данной статье я собираюсь рассмотреть вариант релизации AJAX-обмена на основе компонентов:

1. Клиентская библиотека – jQuery,

2. Серверный ответчик – Generic Handler (*.ashx),

3. Способ сериализации – DataContractJsonSerializer,

4. Тип данных – HTML-разметка, упакованная в JSON-строку.

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

И опять все по пунктам:

1. Мы используем клиентскую библиотеку jQuery. Скачать ее последнюю версию можно на официальном сайте – ищите в поисковиках. Помимо многочисленных полезных функций, есть в ней и функции для работы с AJAX. Я же приведу готовый скрипт, который делает запрос к серверу, и получив ответ от него, отрисовывает результат на странице:

var NetworkDirectoryControl = {
 LoadDirectory: function (path) {
 var url = 'NetworkDirectoryHandler.ashx?root=' + path;
 $.ajax({
 type: "POST",
 url: url,
 success: NetworkDirectoryControl.OnLoadDirectorySuccess,
 error: NetworkDirectoryControl.OnLoadDirectoryError
 });
 },
 OnLoadDirectorySuccess: function (transport) {
 var data = $.parseJSON(transport);
 document.getElementById("NetworkDirectoryControl.ListContainer").innerHTML = data.Control;
 },
 OnLoadDirectoryError: function () {
 }
};

Функция NetworkDirectoryControl.LoadDirectory(path) просто делает асинхронный POST-запрос к серверному ответчику NetworkDirectoryHandler.ashx передавая ему в качестве GET-параметра переменную root.

При успешном выполнении запроса вызывается обратная функция NetworkDirectoryControl.OnLoadDirectorySuccess(transport), у которой аргумент transport содержит JSON-строку, возвращенную сервером. При помощи $.parseJSON мы преобразуем строку в JS-объект, после чего отображаем поле объекта Control в элементе DIV ID=”NetworkDirectoryControl.ListContainer”.

2. Серверный ответчик

<%@ WebHandler Language="C#" %>

using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.IO;
using System.Text;

/// <summary>
/// Handler which returns folder markup or file content stream due to requested path
/// </summary>
public class NetworkDirectoryHandler : IHttpHandler
{
 public void ProcessRequest(HttpContext context)
 {
 string RequestedPath = NetworkDirectoryControlUtil.DecodeStringForJScript(context.Request.QueryString["root"]);
 if (RequestedPath == null)
 {
 RequestedPath = @"Root:\";
 }

 if (RequestedPath.EndsWith(@"\"))
 {
 // Process directory request
 context.Response.ContentType = "text/plain";
 NetworkDirectoryControlJSONResult jsonResult = new NetworkDirectoryControlJSONResult();
 jsonResult.Control = NetworkDirectoryControlUtil.RenderControl(NetworkDirectoryControlUtil.RootDirectory, RequestedPath);
 string response = NetworkDirectoryControlUtil.SerializeToJSON<NetworkDirectoryControlJSONResult>(jsonResult);
 context.Response.Write(response);
 }
 else
 {
 // Process file request
 string FileName = RequestedPath.Remove(0, RequestedPath.LastIndexOf(@"\") + 1);
 context.Response.ContentType = "application/octet-stream";
 context.Response.AddHeader("Content-Disposition", @"attachment; filename=""" + FileName + @"""");
 context.Response.ContentEncoding = System.Text.Encoding.UTF8;
 context.Response.Charset = "UTF-8";
 try
 {
 context.Response.WriteFile(NetworkDirectoryControlUtil.GetAbsolutePath(NetworkDirectoryControlUtil.RootDirectory, RequestedPath));
 }
 catch
 {
 // In case of error - do nothing
 }
 }
 }

 public bool IsReusable
 {
 get
 {
 return false;
 }
 }

}

Метод ProcessRequest объекта NetworkDirectoryHandler срабатывает всякий раз при обращении к нему. В качестве аргумента он получает свой HTTP-Context, в котором хранятся все полученные GET и POST параметры, обработав их, мы можем в объект Response записать выходные данные обработчика.

В нашем случае мы получаем переменную root, преобразуем ее из формата Base64 методом NetworkDirectoryControlUtil.DecodeStringForJScript, по наличию в конце строки слэша мы определяем был этот запрос к папке или к файлу, и затем, в соответствии с этим либо отрисовываем контрол и отсылаем назад разметку, либо читаем запрошенный файл и пишем его в поток Response.

3. Способ сериализации

public static string SerializeToJSON<T>(T InputObject)
 {
 DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(T));
 MemoryStream stream = new MemoryStream();
 serializer.WriteObject(stream, InputObject);
 stream.Seek(0, SeekOrigin.Begin);
 StreamReader reader = new StreamReader(stream);
 return reader.ReadToEnd();
 }

Сериализатор DataContractJsonSerializer работает на основе контрактов. Контрактом является заголовочное описание класса, описанного атрибутом [DataContract], и некоторые члены которого помечены атрибутом [DataMember]. Контрактный JSON-сериализатор работает только с такими классами и сериализует только те их члены, которые помечены атрибутом. Остальные члены игнорируются.

[DataContract]
public class NetworkDirectoryControlJSONResult
{
 [DataMember]
 public string Control;
}

4. В качестве типа данных мы выбрали HTML-разметку, упакованную в JSON-строку. JSON-сериализацию мы уже рассмотрели, осталось узнать, как отрисовывать контролы вне страничного контекста, то есть “вручную”.

public static string RenderControl(string RootDirectory, string RelativeDirectory)
 {
 NetworkDirectoryControl control = new NetworkDirectoryControl();
 ((INetworkDirectoryControl)control).RootDirectory = RootDirectory;
 ((INetworkDirectoryControl)control).RelativeDirectory = RelativeDirectory;
 Page page = new Page();
 page.Controls.Add(control);
 StringWriter sw = new StringWriter();
 HtmlTextWriter htw = new HtmlTextWriter(sw);
 HttpContext.Current.Server.Execute(page, htw, false);
 return sw.ToString();
 }

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

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

Здесь я выкладываю ссылку на готовый работоспособный проект в Visual Studio 2010. Весь AJAX-функционал проекта кроется в файлах: NetworkDirectoryHandler.ashx, jquery-1.4.1.min.js, NetworkDirectory.js и NetworkDirectoryClasses.cs.

Запускайте, комментируйте, спрашивайте.

Реализация чата на ASP.NET с использованием Long Polling

Вячеслав Гринин, March 13, 2010

Бродя по просторам интернета, на одном из сайтов я увидел простой чат. Просмотрев страницу в FireBug я понял, что никакими апплетами там и не пахнет, а значит чат реализован на простом JavaScript.

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

И встал передо мной вопрос – как они это сделали? Как они обеспечили такую высокую скорость отдачи свежих данных? Ведь веб-сервер на то и сервер, что может лишь отвечать на запросы пользователя, но уж никак не самостоятельно инициировать запросы.

Непродолжительный поиск по интернетам дал мне ответ. И ответ этот “Long Polling“, а встречается еще и другое название технологии – Comet.

Итак, как же это работает?

По сути, это все тот же старый добрый AJAX, и использование объекта XmlHttpRequest. С одной большой разницей, заключенной на серверной стороне. Разница состоит вот в чем. Клиент посылает XHR-запрос и длительное время ожидает ответа сервера. А “длительное время” отклика обеспечивает сам сервер, который не сразу отдает данные клиенту, а лишь только тогда, когда у него в очереди появляются свежие данные, то есть когда ему действительно есть что сказать.

Клиент, дождавшись ответа, обрабатывает его (например, отображает сообщение на странице), а затем, без промедления, снова запрашивает сервер. И снова ждет.

Если же подав запрос клиент так и не дождался ответа из-за таймаута, то он снова запрашивает сервер. А откуда этот таймаут? А просто серверу нам нечего ответить, вот он и не отдает клиенту результат запроса. Ждет. Но не дождавшись, разрывает соединение по таймауту. Такая вот несложная схема.

Это и называется Long Polling, то есть – длительный опрос.

Но от слов к делу. Сейчас мы изготовим серверную и клиентскую часть простого интернет-чата, построенного на описанной технологии. Мы подробно рассмотрим работу всего серверного и клиентского кода и даже в конце статьи получим ссылку на вполне работоспособный продукт. Сразу скажу, у него есть некоторые ограничения. Вы, например, не сможете его использовать в режиме сильной нагрузки, то есть наличии одновременно большого количества клиентов. В нем нет схемы очистки “потерянных” соединений. Это лишь работоспособный учебный пример. Все недостающие аспекты кода предлагаю вам на самостоятельную доработку.

Итак… Без особых размышлений приведу здесь листинг кода и объясню принцип его действия.

using System;
using System.Collections.Generic;

// Класс описывающий одно сообщение от клиента и
// метод его сериализации
public class CometMessage
{
 public string UserName;
 public string Message;
 public string Serialize()
 {
 return "{'user': '" + UserName + "', 'message': '" + Message + "'}";
 }
}

// Собственно, серверная часть
public static class CometServer
{
 // вспомогательный объект для блокировки ресурсов
 // многопоточного приложения
 private static Object _lock = new Object();

 // Список, хранящий состояние всех подключенных клиентов
 private static List<CometAsyncState> _clientStateList =
   new List<CometAsyncState>();

 // Возвращаем сообщение каждому подключенному клиенту
 public static void PushMessage(CometMessage message)
 {
 lock (_lock)
 {
 // Пробегаем по списку всех подключенных клиентов
 foreach (CometAsyncState clientState in _clientStateList)
 {
 if (clientState.CurrentContext.Session != null)
 {
 // И пишем в выходной поток текущее сообщение
 clientState.CurrentContext.Response.Write(message.Serialize());
 // После чего завершаем запрос - вот именно после этого результаты
 // запроса пойдут ко всем подключенным клиентам
 clientState.CompleteRequest();
 }
 }
 }
 }

 // Срабатывает кажды раз при запуске клиентом запроса Long poll
 // так как при этом HttpContext клиента изменяется, то надо обновить
 // все изменившиеся данные клиента в списке, идентифицируемом по
 // гуиду, который у клиента в течение работы остается постоянным
 public static void UpdateClient(CometAsyncState state, String guid)
 {
 lock (_lock)
 {
 // ищем клиента в списке по его гуиду
 CometAsyncState clientState = _clientStateList.Find(s => s.ClientGuid
   == guid);
 if (clientState != null)
 {
 // и если он нашелся, то обновляем все его параметры
 clientState.CurrentContext = state.CurrentContext;
 clientState.ExtraData = state.ExtraData;
 clientState.AsyncCallback = state.AsyncCallback;
 }
 }
 }

 // Регистрация клиента
 public static void RegicterClient(CometAsyncState state)
 {
 lock (_lock)
 {
 // Присваиваем гуид и добавляем в список
 state.ClientGuid = Guid.NewGuid().ToString("N");
 _clientStateList.Add(state);
 }
 }

 // Разрегистрация клиента
 public static void UnregisterClient(CometAsyncState state)
 {
 lock (_lock)
 {
 // Просто удаляем его из списка
 _clientStateList.Remove(state);
 }
 }
}

Итак:

6-14 строки – инкапсулируют класс сообщения от пользователя, как видим, там еще есть метод, преобразующий объект в JSON-строку. Вообще, по-хорошему, здесь надо пользоваться нормальным JSON-сериализатором, а не городить строку вручную.

Далее идет описание класса CometServer в котором и живет вся серверная логика LongPolling-технологии.

Заметим, что в переменной _clientStateList (строка 24) хранятся все подключенные на данный момент к серверу клиенты.

Метод PushMessage (строка 24) пробегает по всему списку клиентов и пишет в выходной поток каждого переданное ему в качестве аргумента сообщение, естественно, предварительно сериализовав его. После чего для каждого клиента в обязательном порядке вызывается метод CompleteRequest(), который завершает асинхронный запрос, передавая его результаты клиенту.

Метод UpdateClient (строка 51) по полученному им гуиду клиента, находит клиента в списке clientStateList и обновляет все его параметры, такие как HttpContext-например. Этим мы обеспечиваем всегда актуальное состояние списка клиентов, после каждого их реконнекта.

Метод RegicterClient (строка 69) /опечатка вышла в его имени, сами исправите при необходимости:)/ добавляет нового клиента в очередь.

Метод UnregisterClient (строка 80) соответственно – удаляет клиента из очереди. Что вполне законно. Зачем уведомлять клиента о новых сообщениях в чате, если он давно покинул чат?

Дальше идет описание класса CometAsyncState. Зачем мы его создали? Да чтобы хранить такие параметры клиента, как CurrentContext, AsyncCallback и ClientGuid. Ну то есть, чтобы отдельный поток, порожденный в пуле потоков и отвечающий за одного клиента, мог собственно функционировать и взаимодействовать с клиентом. Этот класс, по большому счету, всего лишь набор заглушек для методов интерфейса IAsyncResult плюс метод CompleteRequest(), который вызывает callback-функцию при завершении потока.

using System;
using System.Threading;
using System.Web;

public class CometAsyncState : IAsyncResult
{
    // Чтобы было где хранить все это и был создан
    // класс-наследник от IAsyncResult
    public HttpContext CurrentContext;
    public AsyncCallback AsyncCallback;
    public object ExtraData;
    public string ClientGuid;
    private Boolean _isCompleted;

    // Конструктор
    public CometAsyncState(HttpContext context,
      AsyncCallback callback, object data)
    {
        CurrentContext = context;
        AsyncCallback = callback;
        ExtraData = data;
        _isCompleted = false;
    }

    // Завершим запрос
    public void CompleteRequest()
    {
        // При завершении запроса просто выставим флаг
        // что он завершен
        // и вызовем callback
        _isCompleted = true;
        if (AsyncCallback != null)
        {
            AsyncCallback(this);
        }
    }

    #region IAsyncResult Members
    // И снова видим набор заглушек для интерфейса IAsyncResult

    public Boolean CompletedSynchronously
    {
        get
        {
            return false;
        }
    }

    public bool IsCompleted
    {
        get
        {
            return _isCompleted;
        }
    }

    public object AsyncState
    {
        get
        {
            return ExtraData;
        }
    }

    public WaitHandle AsyncWaitHandle
    {
        get
        {
            return new ManualResetEvent(false);
        }
    }
    #endregion
}

Осталась малость – клиентский JavaScript. Ниже я приведу его с моими подробными комментариями в самом коде и не буду его так детально разжевывать. Потому что весь он разжеван в начале статьи, где я описывал принцип работы LongPolling.

var clientGuid

$(document).ready(function() {
 // Подключаемся после загрузки страницы,
 // запускаем первый long polling
 Connect();
});

$(window).unload(function() {
 // При выгрузке страницы - запрашиваем сервер об отключении
 // клиента для экономии ресурсов
 Disconnect();
});

// Посылает lonp poll - запрос серверу
function SendRequest() {
 var url = './comet.ashx?guid=' + clientGuid;
 $.ajax({
 type: "POST",
 url: url,
 // Если запрос завершился успехом, значит сервер сообщил
 // о новых событиях - обрабатываем их
 success: ProcessResponse,
 // При ошибке (например таймауте), снова рекурсивно
 // посылаем запрос обеспечивая тем самым непрерывный
 // процесс прослушки серверных событий
 error: SendRequest
 });
}

// Регистрируемся на сервере
function Connect() {
 var url = './comet.ashx?cmd=register';
 $.ajax({
 type: "POST",
 url: url,
 success: OnConnected,
 error: ConnectionRefused
 });
}

// Разрегистрируемся на сервере
function Disconnect() {
 var url = './comet.ashx?cmd=unregister';
 $.ajax({
 type: "POST",
 url: url
 });
}

// Обработка сообщений, принятых с сервера
function ProcessResponse(transport) {
 eval('var d=' + transport + ';');
 document.getElementById("content").innerHTML +=
    ' <strong>' + d.user + '</strong> : "' + d.message + '"<br/>';
 // После отображения результатов запроса -
 // снова циклично делаем запрос.
 SendRequest();
}

// После регистрации на сервере сохраняем наш guid и
// посылаем первый long poll запрос на сервер
function OnConnected(transport) {
 clientGuid = transport;
 SendRequest();
}

// Если подключиться не удалось, то ждем три мекунды
// и опять пробуем подключиться
function ConnectionRefused() {
 $("#content").html("не удалось подключиться к серверу.
    Попробуем через 3 секунды...");
 setTimeout(Connect(), 3000);
}

// Отправка сообщения на сервер
function clickSendMessage() {
 var userName = document.getElementById("userName").value;
 var message = document.getElementById("message").value;
 var url = './comet.ashx?cmd=send&message=' + message + '&user=' + userName;
 $.ajax({
 type: "POST",
 url: url
 });
}

Ну и разумеется, код серверного хэндлера comet.ashx, который обрабатывает команды клиентского скрипта. Скажу сразу, хэндлер этот асинхронный, а потому унаследован не от IHttpHandler, а от IHttpAsyncHandler

< %@ WebHandler Language="C#" Class="CometAsyncHandler" %>

using System;
using System.Web;
using System.Threading;

public class CometAsyncHandler : IHttpAsyncHandler,
  System.Web.SessionState.IRequiresSessionState
{
    #region IHttpAsyncHandler Members

    public IAsyncResult BeginProcessRequest(HttpContext ctx,
      AsyncCallback cb, Object obj)
    {
        // Готовим объект для передачи его в QueueUserWorkItem
        CometAsyncState currentAsyncState =
          new CometAsyncState(ctx, cb, obj);

        // Добавляем в тредпул новый ждущий поток
        ThreadPool.QueueUserWorkItem(new WaitCallback(RequestWorker),
           currentAsyncState);

        return currentAsyncState;
    }

    public void EndProcessRequest(IAsyncResult ar)
    {
    }

    #endregion

    #region IHttpHandler Members
    // IHttpHandler Members - просто пустые заглушки,
    // так как нам не требуется реализация синхронных методов

    public bool IsReusable
    {
        get
        {
            return true;
        }
    }

    public void ProcessRequest(HttpContext context)
    {
    }

    #endregion

    // Основная функция рабочего потока
    private void RequestWorker(Object obj)
    {
        // obj - второй параметр
        // при вызове ThreadPool.QueueUserWorkItem()
        CometAsyncState state = obj as CometAsyncState;

        string command =
          state.CurrentContext.Request.QueryString["cmd"];
        string guid =
          state.CurrentContext.Request.QueryString["guid"];

        switch (command)
        {
            case "register":
                // Регистрируем клиента в очереди сообщений
                CometServer.RegicterClient(state);
                state.CurrentContext.Response.Write(
                  state.ClientGuid.ToString());
                state.CompleteRequest();
                break;
            case "unregister":
                // Удаляем клиента из очереди сообщений
                CometServer.UnregisterClient(state);
                state.CompleteRequest();
                break;
            case "send":
                // Отсылка сообщения
                string message =
                  state.CurrentContext.Request.QueryString["message"];
                string userName =
                  state.CurrentContext.Request.QueryString["user"];
                CometServer.PushMessage(new CometMessage() {
                  Message = message, UserName = userName });
                state.CompleteRequest();
                break;
            default:
                // При реконнекте клиента
                if (guid != null)
                {
                    CometServer.UpdateClient(state, guid);
                }
                break;

        }
    }
}

Основная логика программы кроется в методе RequestWorker (строки 51-95), который в зависимости от полученной с клиента команды, выполняет либо регистрацию/разрегистрацию клиента, либо отправку сообщения. Либо обновление данных о клиенте при его реконнекте.

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

Вот и все. Надеюсь, что статья оказалось полезной для вас.

Upd:
Оказалось, что при существовании какой-либо информации в сессии LongPolling-чат перестает корректно работать, обрывая соединения, и не обеспечивая доставку сообщений конечным пользователям. Решение проблемы описано в статье HTTPHandler : IRequiresSessionState halts execution of pages, а если кратко – следует наследовать CometAsyncHandler от System.Web.SessionState.IReadOnlySessionState, а не от System.Web.SessionState.IRequiresSessionState. Спасибо пользвателю Алеша за предоставленную информацию.

AJAX. Загрузка файлов.

Вячеслав Гринин, February 20, 2010

Немного странное название статьи… Мы помним, что AJAX это асинхронный JavaScript, построенный на основе объекта XmlHttpRequest. Отличная технология! С некоторыми ограничениями. Одно из них – отсутствие кросдоменности – мы уже научились обходить в статье Кросдоменный JavaScript (JSONP). Но есть у этой замечательной технологии еще один недостаток – она не позволяет асинхронно загружать файлы. Ну вот не умеет объект XmlHttpRequest загружать файлы!

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

Для загрузки файлов мы используем технологию, которая называется Remote Scripting посредством IFRAME. А в общем, технология проста. Для загрузки файла мы все также будем использовать обычную форму FORM, в которой разместим самый обычный INPUT TYPE=’FILE’. Все как и прежде в случае синхронной загрузки файла. И вот здесь и начинаются отличия. А отличие состоит в том, что у формы target указывает на скрытый на странице IFRAME, который по сути и перезагрузится во время операции. Мало того, в IFRAME мы вернем результат операции в виде вызова простого JavaScript-коллбэка, который завершит операцию загрузки.

Итак, от слов к делу. Далее я приведу пример кода, который и реализует вышеописанную технологию. Код этот очень простой.

<html>
<head>
<script type="text/javascript">
 function onResponse(d) {
 eval('var obj = ' + d + ';');
 alert('Файл ' + obj.filename + (obj.success ? " " : " НЕ ") + 
    "загружен.");
 }
 </script>
</head>

<iframe id="rFrame" name="rFrame" style="display: none">
</iframe>

<form action="handler.php" target="rFrame" method="POST" 
   enctype="multipart/form-data">
<input type="file" name="loadfile">
<input type='submit' value='Загрузить'>
</form>

</html>

Здесь мы видим функцию onResponse, которая получает результат обработки файла(результат этот мы формируем на сервере). Еще мы видим IFRAME с идентификатором rFrame, про который мы говорили выше. И видим мы также саму форму загрузки файла.

У формы action=”handler.php” указывает на серверный обработчик, и не забудьте указать enctype=”multipart/form-data” иначе форма не сможет загружать файлы.

А дальше я приведу серверный код, содержимое файла handler.php.

<?php
 function jsOnResponse($obj)
 {
 echo '
 <script type="text/javascript">
 window.parent.onResponse("'.$obj.'");
 </script>
 ';
 }

 $dir = '/home/path/path/path';
 $name = basename($_FILES['loadfile']['name']);
 $file = $dir . $name;

 $success = move_uploaded_file($_FILES['loadfile']['tmp_name'], $file);
 jsOnResponse("{'filename':'" . $name . "', 'success':'" . $success . "'}");

?>

Этот код просто сохраняет файл в папку на сервере, объяснять саму загрузку нет смысла – она стандартна. Обращу лишь внимание, что после загрузки файла вызывается функция jsOnResponse которая просто пишет в выходной поток вызов JavaScript-функции window.parent.onResponse($obj). Клиентский IFRAME, приняв этот код, выполнит его, вызвав функцию onResponse у родительской страницы фрейма.

Вот и все. Пользуйтесь. Здесь можно скачать работающий код приведенного в статье примера.

Используя уникальную систему поиска Price-AZ можно заказать любые запчасти Фольксваген и Ауди на сайте ООО “Польга”. Теперь стало гораздо удобнее найти нужную запчасть не только по номеру, но и по ее названию. При магазине есть также удобный автосервис. Кстати, система Price-AZ позволяет найти запчасть на автомобиль любой марки.

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

AJAX с помощью jQuery и ASP.NET web-сервисов

Вячеслав Гринин, September 21, 2009

Если вы создаете сайты под ASP.NET, то возможно вам уже довелось использовать AJAX-framework, предоставляемый Microsoft. Тот самый, что использует ScriptManager, rfк в примере, приведенном ниже:

1) Содержимое страницы Default.aspx:

<asp:ScriptManager ID="MainSM" ScriptMode="Debug" runat="server">
 <Services>
 <asp:ServiceReference Path="~/usersrv.asmx" InlineScript="false" />
 </Services>
 </asp:ScriptManager>

2) Содержимое файла script.js:

// вызов ajax-метода GetPoint
usersrv.GetPoint(id,OnUSGetPointSucc,OnUSGetPointErr);
// callback успешного вызова
function OnUSGetPointSucc(res)
{
 alert('success');
}
// callback ошибки
function OnUSGetPointErr(err)
{
 alert('success');
}

3) Содержимое файла usersrv.asmx:

<%@ WebService Language="C#" CodeBehind="~/App_Code/usersrv.cs" %>

4) Содержимое файла usersrv.cs:

[WebService(Namespace = "http://tempuri.org/")]
[System.Web.Script.Services.ScriptService]
public class usersrv : System.Web.Services.WebService
{
[WebMethod]
public Dictionary<string, object> GetPoint(long p_id)
 {
 Dictionary<string, object> res = new Dictionary<string, object>();
 try
 {
   // готовим возвращаемые параметры и сохраняем их в res
 }
 catch (Exception)
 {
 res["Error"] = U.GTFR("userpoints_err");
 }
 return res;
 }
}

Этот метод достаточно прост и удобен, если бы не одно “но” – он весит 132 Кб. И обладает еще одним интересным свойством: если в нашем проекте есть несколько страниц, использующих AJAX, то каждая из них будет заново загружать этот скрипт, потому что при загрузке скрипта на странице используется примерно следующий URL http://domainname/ScriptResource.axd?d=TvjJs2RlM8MX3pIUhEsdnZKUzGh-9Wr9nvNBZJbxf-xzq-Wvzbpj6FfMJqZqOPR8Pku52J2zBqleUlYtVE8XCyfyo3kcbSnhbwzYc2LkdfQ1&t=ffffffffee41303f и для каждой страницы в пределах вашего проекта будет свой ScriptManager, а значит и свой уникальный ID в query-string. А значит скрипт этот для каждой страницы будет браться не из кэша, а запрашиваться заново с сервера.

В общем, мне все это не понравилось и я заинтересовался, как можно сэкономить трафик и перейти на AJAX.jQuery. Тем более, что он уже использовался в этом же проекте для других целей. Сказано – сделано! И вот какое решение я нашел.

Собственно, модификации пришлось подвергнуть как сами клиентские JavaScript-ы, так и методы, вызываемые на стороне сервера. Связано это с тем, что мне хотелось передавать в качестве параметров для web-сервиса сложные объекты, а не просто строки и целые значения. И получать в ответ хотелось тоже сложные объекты, а не просто строки.

Все это можно легко и просто сделать, передавая как в ту так и в другую сторону все же строки, но строки в формате JSON, который является родным для JavaScript. Да к тому же ASP.NET обладает встроенными возможностями сериализации/десериализации JSON.

Правда, для сериализации объекта в JSON на клиентской стороне пришлось использовать еще одну дополнительную библиотеку json2.js. Таким образом, обе библиотеки весят около 67 Кб. Да плюс ко всему в случае нескольких ajax-страниц в проекте, скрипт загрузится всего один раз, и для остальных страниц будет браться из кэша браузера.

Но от слов – к делу!  Вот содержимое скрипта, выполняемого на клиентской стороне:

// готовим передаваемые параметры в переменную par, здесь - всего лишь эмуляция
 var par = new Object();
 par.a = 5;
 par.b = 'data';
 par.c = new Array();
 par.c[0] = 5;
 par.c[1] = 10;
 // сериализуем объект
 var ser = JSON.stringify(par);
 // формируем ajax-запрос
 $.ajax({
 type: "POST",
 url: "usersrv.asmx/GetPoint",
 data: "{'par':'"+ser+"'}",
 contentType: "application/json; charset=utf-8",
 dataType: "json",
 success: function(r) {
 var res=r.d;
 // сериализуем для наглядности полученные данные и выводим их 
на страницу
 // в реальном приложении здесь мы просто будем использовать готовые 
данные
 // в переменной res без сериализации
 document.getElementById("result").innerHTML=JSON.stringify(r);
 },
 error: function(err) {
 // обработка ошибки
 alert('error: '+err.responseText);
 }
 });
 }

А вот содержимое usersrv.cs:

using System;
using System.Collections;
using System.Linq;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml.Linq;
using System.Web.Script.Serialization;

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.Web.Script.Services.ScriptService]
public class usersrv : System.Web.Services.WebService
{
 public usersrv() { }

 [WebMethod]
 public ResponseType GetPoint(string par)
 {
 ResponseType response = new ResponseType();
 RequestType request = new RequestType();
 try
 {
 // десериализуем полученные параметры
 JavaScriptSerializer ser = new JavaScriptSerializer();
 request = ser.Deserialize<RequestType>(par);
 }
 catch (Exception)
 {
 response.isError = true;
 response.message = "Parameters is invalid";
 return response;
 }

 // преобразуем переданные параметры и готовим ответ
 // здесь - всего лишь эмуляция обработки данных
 if (request.c != null && request.a == request.c.Length)
 {
 response.isError = false;
 response.message = "That's good";
 }
 else
 {
 response.isError = true;
 response.message = "Length is not the same";
 }
 return response;
 }
}

А вот описание двух воспомогательных классов:

public class RequestType
{
    public int a;
    public string b;
    public int[] c;
}
public class ResponseType
{
    public string message;
    public bool isError;
}

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

Ссылка на архив с готовым проектом здесь.

AJAX. 2 – Подробнее об XMLHttpRequest

Вячеслав Гринин, January 30, 2009

В статье AJAX. 1 – Что это такое? я рассказал про то, в каких случаях можно использовать AJAX и привел тестовый пример использования AJAX в веб-разработке. Теперь я хочу немного углубиться в подробности работы с объектом XMLHttpRequest и расскажу про его основные свойства и методы. Итак…

Методы объекта XMLHttpRequest:

  • abort() – Прекращает исполнение текущего запроса. При этом объект XMLHttpRequest возвращет readyState=4 и status=0.
  • getAllResponseHeaders()- Возвращает HTTP-заголовки ответа в
    виде строки. Эта строка может выглядеть, например, так:
    Date: Fri, 30 Jan 2009 13:57:48 GMT Server: Apache/2.2.4 (Win32) mod_ssl/2.2.4 OpenSSL/0.9.8d PHP/5.2.4 X-Powered-By: PHP/5.2.4 Content-Length: 91 Keep-Alive: timeout=5, max=98 Connection: Keep-Alive Content-Type: text/xml
  • getResponseHeader(“header-name”) – Возвращает заголовок “header-name” ответа. Для случая getResponseHeader(‘Content-Type’) она вернет
    text/xml
  • open(“method”, “URL”, async-flag, “user-name”, “password”) – Инициализирует параметры запроса. Если в качестве “method” используется строка “POST”, то метод send(‘content’) в качестве параметра должен принимать данные, передаваемые методом POST. Если же “method”=”GET”, то параметры обработчику передаются в параметре “URL” метода open()
  • send(“content”) – Выполняет HTTP-запрос.
  • setRequestHeader(“header-name”, “header-value”) – Добавляет в запрос HTTP-заголовок.

Свойства объекта XMLHttpRequest::

  • onreadystatechange – Установка функции обратного вызова, которая будет обслуживать изменение состояния запроса.
  • readyState – Возвращает состояние запроса
    0 – не инициализирован
    1 – идет отправка запроса
    2 – запрос отправлен
    3 – идет обмен
    4 – обмен завершен
  • responseText – Содержит ответ сервера в виде строки
  • responseXML – Содержит ответ сервера в виде XML-документа
  • status – Возвращает код состояния запроса
  • statusText – Возвращает сообщение о состоянии запроса

Здесь расположен архив с исходными кодами тестового примера, который демонстрирует использование всех вышеприведенных свойств и методов.

Поиск по блогу:
Подписаться:
Популярные:
Облако тегов:
Разное:
Счетчик: