Показаны сообщения с ярлыком ASP.NET 2.0 AJAX. Показать все сообщения
Показаны сообщения с ярлыком ASP.NET 2.0 AJAX. Показать все сообщения

пятница, октября 07, 2011

Пишем ajax available user control

Исходные коды к статье
Привет.


Сегодня хотелось бы поделиться одной вкусной штукой, которая может оказаться полезной во многих сценариях в работе с asp.net web forms, возможно, в SharePoint тоже окажется полезной.
Вкратце, суть идеи в следующем: хочется со стороны клиента, из клиентского сценария, вызвать серверный метод....но....естественно, все это делается через коллбеки и реализацию ICallbackEventHandler....хочется сделать это один раз для всех возможных сценариев и не думать каждый раз о реализации указанного интерфейса, думать о сериализации/десериализации аргументов и результатов от сервера в строку и прочими делами.... хочу просто тупо вызвать серверный метод по его имени, получить ответ, причем ответ хочу получить в объектном виде, не хочу получать строки и потом парсить их на стороне сервера. Вот такое желание. Чтобы сразу увидеть результат того, что получится, приведу в начале статьи кусочек кода.

Итак, объявляем серверный user control:


public partial class DemoUserControl : AjaxWebModule
    {
        #region demo methods here
        public AjaxResponse MyDemoMethod1(string arg1, string arg2, int arg3, bool arg4)
        {
            AjaxResponse result = new AjaxResponse
            {
                AdditionalSettings = String.Format("This is response to callback with parameters {0}/{1}/{2}/{3} from server side", arg1, arg2, arg3, arg4),
                CurrentPage = 0,
                Records = new List<object>() 
                {
                    new 
                    {
                        Date = DateTime.Now,
                        Data = int.MinValue
                    }
                },
                TotalRecords = 1,
                TotalPages = 1
            };

            return result;
        }

        public AjaxResponse MyDemoMethod2(int arg1, bool arg2)
        {
            AjaxResponse result = new AjaxResponse
            {
                AdditionalSettings = String.Format("This is response to callback with parameters {0}/{1} from server side", arg1, arg2),
                Records = new List<object>() 
                {
                    new 
                    {
                        Date = DateTime.Now,
                        Data = int.MinValue,
                        OtherProperties = new List<object>{1, 2, 3}
                    }
                }
            };

            return result;
        } 
        #endregion
    }
И его серверный маркап:

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="DemoUserControl.ascx.cs" Inherits="AjaxAvailableUserControls.Application.AjaxUserControls.DemoUserControl" %>
<input type="button" onclick="click_handler();" value="click me to call server side method 1" id="btnDemo1"/><br />
<input type="button" onclick="click_handler();" value="click me to call server side method 2" id="btnDemo2"/><br />
<script language="javascript" type="text/javascript">
    
    $(document).ready(function()
    {
            $("#btnDemo1").click(function() 
            {
                jQuery.execute("MyDemoMethod1",
                { "arg1": "test string 1", "arg2": "test strign 2", "arg3": 2, "arg4": false },
                {
                    onSuccess: function (data) {
                        debugger;
                        alert("settings:" + data.AdditionalSettings + ";currentPage:" + data.CurrentPage.toString());
                    },
                    onError: function (exception) { alert(exception); }
                });
            });

            $("#btnDemo2").click(function () {
                jQuery.execute("MyDemoMethod2",
                { "arg1": 1, "arg2": false },
                {
                    onSuccess: function (data) {
                        debugger;
                        alert("settings:" + data.AdditionalSettings + ";currentPage:" + data.CurrentPage.toString());
                    },
                    onError: function (exception) { alert(exception); }
                });
            });

    });
</script>
Как видите, все достаточно просто. При разработке code-behind класса нашего .ascx контрола нам всего лишь нужно отнаследоваться от AjaxWebModule и объявить те методы, которые мы хотим использовать на стороне клиента.

При этом сохраняется сигнатура вызова метода со стороны клиента. Важно только то, что данный метод должен быть объявлен как public и должен возвращать результат в виде объекта класса AjaxResponse.  В вышеприведенном коде мы производим вызов двух серверных методов  - MyDemoMethod1 и MyDemoMethod2 - передача параметров происходит поименновано путем указания имени аргумента и его значения:

{ "arg1": "test string 1", "arg2": "test strign 2", "arg3": 2, "arg4": false }  - (string arg1, string arg2, int arg3, bool arg4)


{ "arg1": 1, "arg2": false } - (int arg1, bool arg2)

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

Итак, в вышеприведенном клиентском сценарии я умышленно оставил вставки debugger, чтобы продемонстировать в конечном итоге результат.

Кликаем по первой кнопке:




Как видно, со стороны клиента мы вызываем серверный метод MyDemoMetho1 и получаем результат в объектном виде, без всяких преобразований - магия :), более того, мы имеем возможность обработать как нормальное выполнение метода, так и его ошибку в случае какой-либо исключительной ситуации.

Вызов со стороны клиента подробнее:

jQuery.execute("MyDemoMethod1",
                { "arg1": "test string 1", "arg2": "test strign 2", "arg3": 2, "arg4": false },
                {
                    onSuccess: function (data) {
                        debugger;
                        alert("settings:" + data.AdditionalSettings + ";currentPage:" + data.CurrentPage.toString());
                    },
                    onError: function (exception) { alert(exception); }
                });


Как видите, в данном случае  вызов серверного метод обернут в jquery-плагин .execute, код которого доступен в исходном коде, сделано это исключительно просто ради удобства.

Результат вызова мы уже видели на скриншоте выше, вот что мы получим на клиентской стороне в javascript путем обращения к переменной data в onSuccess обработчике:









Как видно, результат нормально десериализован, мы имеем на руках как обычные свойства объекта data, так и коллекцию вложенных объектов Records.

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




Собственно, все, надеюсь вам понравилась идея? Далее идет объяснение некоторых моментов.


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


Итак, его сигнатура:


///
/// Base class for all modules contained ajax logic
///
public class AjaxWebModule : UserControl, ICallbackEventHandler
{
..................
}
Здесь все знакомо, данный класс реализует интерфейс ICallbackEventHandler, подробнее о нем останавливаться не буду, о нем уже писал достаточно подробно ранее. Важно, что в реализации текущего интефейса мы можем получить со стороны клиента строку, распарсить ее, выполнить действия, и вернуть строковый результат клиенту. Собственно, само распарсивание строки запроса выполняется в методе:



/// <summary>
        /// Обрабатывает запрос со стороны клиента, вызывает соотвествующий метод из текущего модуля, формирует ответ, json-сериализует его в строку и
        /// отправляет обратно на клиента
        /// </summary>
        /// <param name="e"></param>
        private void ProcessRequest(ClientDataReceivedEventArgs e)
        {
            e.Cancel = false;
            //  Данные со стороны клиента, передаваемые в качестве параметров в наш ajax метод
            //  Параметры должны соотвествовать структуре класса AjaxRequestParameters
            //  e.ClientData

            AjaxRequestParameters parameters = this.ParseParameters(e.ClientData);


            //  вызов серверного ajax-метода
            AjaxResponse response = this.MethodInvoke(parameters);

            //  Сериализованное состояние ответа от сервера, отправляемое клиенту в качестве ответа
            //  e.ServerResponse
            //  формируем ответ клиенту
            JavaScriptSerializer serializer = new JavaScriptSerializer();
            e.ServerResponse = serializer.Serialize(response);
            
        }

Вот здесь мы и встречаем наш AjaxResponse - объект данного класса мы получаем от метода MethodInvoke, который принимает объект класса AjaxRequestParameters,  полученный путем распарсивания строки со стороны клиента. 


Мы пока рассматриваем только серверную логику, до клиентской еще дойдем. Класс AjaxRequestParameters представляет собой следующее:

/// <summary>
    /// Параметры, передаваемый в ajax метод со стороны клиента
    /// </summary>
    public class AjaxRequestParameters
    {

        /// <summary>
        /// Имя вызываемого метода
        /// </summary>
        public string MethodName
        {
            get;
            set;
        }

        /// <summary>
        /// Список ЗНАЧЕНИЙ параметров, которые должны соотвествовать соотвествующим аргументам указанного метода - передаются со стороны клиента в виед пар Ключ-Значение
        /// </summary>
        public Dictionary<string, object> Parameters
        {
            get;
            set;
        }
    }



Здесь мы уже видим название метода, и список параметров/значений, которые будут переданы на выполнение указанному методу. Надеюсь, уже становится понятным, что делает метод MethodInvoke(AjaxRequestParameters parameters) - его задача просто найти указанный метод в текущем объекте юзер-контрола, вызвать его и обернуть результат выполнения в заданный формат. 


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


Почему нам важна сигнатура возвращаемого результата? Только потому, что нам придется данный результат передавать на клиента, а чтобы его передать, нам нужно его сериализовать в строку, потому ответ от сервера должен отвечать определенным требованиям, а именно, он должен отвечать правилам сериализации. В данном примере я просто использовал простые типы данных для объвления интерфейса AjaxResponse, вы можете использовать другой интерфейс.

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



//  Сериализованное состояние ответа от сервера, отправляемое клиенту в качестве ответа
//  e.ServerResponse
//  формируем ответ клиенту
JavaScriptSerializer serializer = new JavaScriptSerializer();
e.ServerResponse = serializer.Serialize(response);



По серверному коду вопросов остаться не должно, теперь можно рассмотреть клиентскую реализацию. Сразу приведу исходный код jquery плагина - execute:



//  core. client scripts library
//  created on 20111007 by smirnov andrey  - duШes
//  #region execute extension method
$.execute = function (methodName, parameters, options) {
    /// <summary>
    ///  Вызывает серверный public-Метод текущего AjaxWebModule
    /// </summary>
    /// <param name="methodName" type="Object">
    ///  Аргумент methodName представляет собой имя вызываемого серверного метода, например,
    ///     "DemoMethod2"
    /// </param>
    /// <param name="parameters" type="Object">
    ///  Объект parameters представляет собой хеш с указанием списка параметров в виде хеш - Имя параметра - Значение, например, 
    ///     { "id", 1 }, { "name", "test" }
    /// </param>
    /// <param name="options" type="Object">
    ///  Объект options представляет собой хеш с указанием OnSuccess handler в случае успешного завершения вызова и OnErrorHandler в случае ошибки, например
    ///  {
    ///              onSuccess: function(data) {
    ///                  alert(deserialized_request.AdditionalSettings);
    ///              }
    ///              onError: function(data) {
    ///                  alert("Exception thrown");
    ///              }
    /// </param>
    /// <returns type="undefined">
    var settings = jQuery.extend({
        control_id: null,
        onSuccess: function (data) { },
        onError: function (data) { alert("Exception thrown ->" + data); }
    }, options || {});

    var localOnSuccessHandler = function (data) {
        /// <summary>
        ///  Данная функция является callback функцией, которая вызывается в случае успешного завершения внутреннего вызова ajaxRequest
        /// </summary>
        /// <param name="data" type="String">
        ///  Результат в виде строки - сериализованное состояние объекта-ответа со стороны серверной части
        /// </param>
        /// <returns type="undefined" />
        var deserialized_request = $.JSON.decode(data);
        settings.onSuccess(deserialized_request);

    }

    var localOnErrorHandler = function (exception) {
        /// <summary>
        ///  Данная функция является callback функцией, которая вызывается в случае НЕуспешного завершения внутреннего вызова ajaxRequest
        /// </summary>
        /// <param name="data" type="String">
        ///  Результат ошибки в виде строки
        /// </param>
        /// <returns type="undefined" />
        settings.onError(exception);
    }

    var arrayParameters = [];
    $.each(parameters, function (name, value) {
        var wrapperParameter =
        { "Key": name,
            "Value": value
        };
        arrayParameters.push(wrapperParameter);
    });
    var request = {
        MethodName: methodName,
        Parameters: arrayParameters
    };

    ajaxRequest(settings.control_id, request, localOnSuccessHandler, localOnErrorHandler);
}
//  #endregion


Что мы здесь видим. Первое - settings для нашего плагина, как нам рекоментует делать jQuery Framework.
http://docs.jquery.com/Plugins/Authoring#DefaultsandOptions


var settings = jQuery.extend({
 control_id: null,
 onSuccess: function (data) { },
 onError: function (data) { alert("Exception thrown ->" + data); }
 }, options || {});

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



var localOnSuccessHandler = function (data) {

 ///    <summary>
 ///        Данная функция является callback функцией, которая вызывается в случае успешного завершения внутреннего вызова ajaxRequest
 ///    </summary>
 ///    <param name="data" type="String">
 ///        Результат в виде строки - сериализованное состояние объекта-ответа со стороны серверной части
 ///    </param>
 ///    <returns type="undefined" />
 var deserialized_request = $.JSON.decode(data);
 settings.onSuccess(deserialized_request);

}
Аналогично, локальный обработчик неуспешного вызова Callback со стороны клиента, здесь десериализовать ничего не нужно, нам нужно получить только сообщение об ошибке со стороны сервера.



var localOnErrorHandler = function (exception) {
  ///    <summary>
  ///        Данная функция является callback функцией, которая вызывается в случае НЕуспешного завершения внутреннего вызова ajaxRequest
  ///    </summary>
  ///    <param name="data" type="String">
  ///        Результат ошибки в виде строки
  ///    </param>
  ///    <returns type="undefined" />
  settings.onError(exception);
}


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


Помните класс AjaxRequestParameters на серверной стороне?  да, это именно его представление, в объект класса AjaxRequestParameters может быть преобразован данный объект со стороны клиента.



var arrayParameters = [];
 $.each(parameters, function (name, value) {
 var wrapperParameter =
 { "Key": name,
  "Value": value
 };
 arrayParameters.push(wrapperParameter);
});

var request = {
 MethodName: methodName,
 Parameters: arrayParameters
};



Все готово, вызываем ajaxRequest:




ajaxRequest(settings.control_id, request, localOnSuccessHandler, localOnErrorHandler);


Стоп, что это !!! что это за ajaxRequest такой?!!

Не волнуйтесь, ajaxRequest - это само ядро callback вызова. Где оно формируется? 
А формируется оно в уже рассмотренном нами классе AjaxWebModule:

/// <summary>
        /// Имя функции, которая будет использоваться для вызова серверного сценария со стороны клиента, принимает один аргумент ARG в виде строки 
        /// </summary>
        [Browsable(true)]
        [Category("Callback Handlers")]
        [DefaultValue("serverCall")]
        [Description("Имя функции, которая будет использоваться для вызова серверного сценария со стороны клиента, принимает один аргумент ARG в виде строки ")]
        public string ServerCallFunctionName
        {
            get
            {
                return "ajaxRequest";
            }
        }



        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);

            #region регистрация server callback function со стороны клиента - здесь формируем wrapper на функцией WebFormdoCallback

            if (!this.Page.ClientScript.IsClientScriptBlockRegistered(this.Page.GetType(), "AjaxRequestWebFormDoCallbackScript"))
            {
                string webFormDoCallbackScript = this.Page.ClientScript.GetCallbackEventReference("control_id", "serialized_request", "localOnSuccessHandler", null, "localOnErrorHandler", true);
                string serverCallScript = "function " + this.ServerCallFunctionName + "(control_id, arg, localOnSuccessHandler, localOnErrorHandler){" +
                    "\r\n" +
                    "var serialized_request = $.JSON.encode(arg);\r\n" +
                    "if (typeof(control_id) == 'undefined' || control_id == null)\r\n" +
                    String.Format("{{ control_id='{0}';}}\r\n", this.UniqueID) +
                    webFormDoCallbackScript +
                    ";\n}\n";

                this.Page.ClientScript.RegisterClientScriptBlock(this.Page.GetType(), "AjaxRequestWebFormDoCallbackScript", serverCallScript, true);
            }
            #endregion

            #region WebformDoCallback script registration

            //  fix проблемы описанной http://www.codeproject.com/KB/aspnet/pendingcallbacks.aspx

            //  регистрируем функцию WebForm_CallbackComplete_SyncFixed, в которой нет ошибки с обращение к переменной i в цикле for
            string callbackCompleteFixScriptName = "WebForm_CallbackComplete_SyncFixed";
            string callbackCompleteFixScript = @"
            function WebForm_CallbackComplete_SyncFixed() {
              // the var statement ensure the variable is not global
                 for (var i = 0; i < __pendingCallbacks.length; i++) {
                    callbackObject = __pendingCallbacks[i];
                    if (callbackObject && callbackObject.xmlRequest && 
               (callbackObject.xmlRequest.readyState == 4)) {
                        
                        if (!__pendingCallbacks[i].async) { 
                            __synchronousCallBackIndex = -1;
                        }
                        __pendingCallbacks[i] = null;
                        var callbackFrameID = '__CALLBACKFRAME' + i;
                        var xmlRequestFrame = document.getElementById(callbackFrameID);
                        if (xmlRequestFrame) {
                            xmlRequestFrame.parentNode.removeChild(xmlRequestFrame);
                        }
                        WebForm_ExecuteCallback(callbackObject);
                    }
                }
            }
            ";
            if (!this.Page.ClientScript.IsClientScriptBlockRegistered(this.Page.GetType(), callbackCompleteFixScriptName))
            {
                this.Page.ClientScript.RegisterClientScriptBlock(this.Page.GetType(), callbackCompleteFixScriptName, callbackCompleteFixScript, true);
            }



            //  заменяем функцию WebForm_CallbackComplete на нашу WebForm_CallbackComplete_SyncFixed и регистрируем ее на момент
            //  полной загрузки страницы
            string onloadScriptName = "pageload_callback_complete_fix";
            string onloadScript = @"
            if (typeof (WebForm_CallbackComplete) == 'function') {
                WebForm_CallbackComplete = WebForm_CallbackComplete_SyncFixed;
            }
            ";

            if (!this.Page.ClientScript.IsStartupScriptRegistered(callbackCompleteFixScriptName))
            {
                this.Page.ClientScript.RegisterStartupScript(this.GetType(), onloadScriptName, onloadScript, true);
            } 
            #endregion
        }




Если мы посмотрим исходный код нашей страницы, то увидим результат выполнения OnPrerender:



function ajaxRequest(control_id, arg, localOnSuccessHandler, localOnErrorHandler){
 var serialized_request = $.JSON.encode(arg);
 if (typeof(control_id) == 'undefined' || control_id == null)
 { control_id='ctl00$Content$DemoUserControl1';}
  WebForm_DoCallback(control_id,serialized_request,localOnSuccessHandler,null,localOnErrorHandler,true);
 }




Собственно, здесь как раз и происходит "заворачивание" нашего "объектного" вызова в строку и передача его в WebForm_DoCallback. 


Сигнатура данного метода подробно описана как в msdn, так и у меня в статьях по выполнения cakkback со стороны клиента ранее. 
Надеюсь, магии и не рассмотренных вопросов тут не осталось, вызов со стороны клиента по прежнему остался прежним, реализуется он через ICallbackEventHandler. 


Передача параметров туда и обратно осталось такой же - а  именно  - только путем передачи строковых значений.


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

Чуть не забыл....если вы внимательно посмотрели на код реализации ajaxRequest не смогли не заметить такой параметр, как controlid (по умолчанию его значение null). Зачем оно? 


Нужен этот параметр только для того, чтобы разделить вызовы из разных user controls, т.е. в том случае, когда на странице есть несколько user controls, из клиенских скриптов которых осуществляется вызов серверных методов, причем, сигнатура их может быть совпадать. Или, в том случае, когда на странице два инстанса нашего  ajax available user control - тут нам и пригодится controlid, чтобы разделить вызовы, идущие к конкретному инстансу user control. Делается это так:



jQuery.execute("DemoMethod1", { "id": 1, "name": "testName" },
 {
  control_id: '<%= UniqueID %>',
  onSuccess: function(data) {
   alert("settings:" + data.AdditionalSettings + ";currentPage:" + data.CurrentPage.toString());
  },
  onError: function(exception) { alert(exception); }
 });

Спасибо за то, что осилили так "много букав", надеюсь статья будет полезной в использовании. В начале статьи приведена ссылка на пример данного решения.

воскресенье, января 17, 2010

CallbackHandlerControl - серверный контрол для выполнения callback-запроса своими руками :)


исходные коды

По мотивам темы Как выполнить callback со стороны клиента на сервер в ASP.NET

Итак, чтобы каждый раз не писать/дублировать код - создаем контрол, обязанностями которого будет выполнение Callback-запроса на сервер и возврат ответа со стороны сервера и возврат его на клиента ...

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

Итак, все сказанное может выглядеть в разметке страницы или вашего user-control следующим образом:



<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="CallbackHandlerControl._Default" %>

<%@ Register assembly="CallbackHandlerControl" namespace="CallbackHandlerControl.Controls" tagprefix="cc1" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>This is a Callback Handle Control Test Page</title>

<script language="javascript" type="text/javascript">
function btnCallServer_clientHandler() {
callServerHandler("argument");
}

function onSuccessfullHandler(result) {
alert(result);
}

function onErrorClientHandler(result) {
}
</script>

</head>
<body>
<form id="form1" runat="server">
<div>

<h1>This is a Callback Handle Control Test Page</h1><br />
<asp:button ID="btnCallServer"
runat="server" text="Script service method invocation"
OnClientClick="btnCallServer_clientHandler();return false;">
</asp:button>

<cc1:CallbackEventHandler ID="callbackHandler" runat="server"
OnClientDataReceived="callbackHandler_ClientDataReceived"
OnSuccessfullClientHandler="onSuccessfullHandler"
OnErrorClientHandler="onErrorClientHandler"
ServerCallFunctionName="callServerHandler"/>

</div>
</form>
</body>
</html>





Обращаем внимание на серверный обработчик серверного события ClientDataReceived - OnClientDataReceived -
данный обработчик будет использован на стороне сервера при получении данных со стороны клиента,
а также для формирования данных, которые будут отправлены на клиента в качестве ответа.



Сам code-behind класс страницы:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace CallbackHandlerControl
{
/// <summary>
/// This is a Callback Handle Control Test Page
/// </summary>
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{

}

#region event handlers
protected void callbackHandler_ClientDataReceived(object sender, CallbackHandlerControl.Controls.ClientDataReceivedEventArgs e)
{
string clientData = e.ClientData;
e.Cancel = false;
e.ServerResponse = DateTime.Now.ToString();
}
#endregion
}
}




Итак, сам текст нашего контрола:


using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace CallbackHandlerControl.Controls
{

/// <summary>
/// Кконтрол выполняет callback обращение на серверную сторону со стороны клиента
/// </summary>
[ToolboxData("<{0}:CallbackEventHandler ID=\"callbackHandler\" runat=\"server\"> </{0}:CallbackEventHandler>")]
public partial class CallbackEventHandler : System.Web.UI.WebControls.WebControl, ICallbackEventHandler
{
#region events
/// <summary>
/// Данное событие будет подниматься в случае получения данных со стороны клиента
/// на сервеной стороне в контроле <see cref="WebSite.WebModules.Common.Callbacks.CallbackEventHandler"/>
/// </summary>
[Browsable(true)]
[Category("Callback Handlers")]
[Description("Данное событие будет подниматься в случае получения данных со стороны клиента на сервеной стороне в контроле WebSite.WebModules.Common.Callbacks.CallbackEventHandler")]
public event EventHandler<ClientDataReceivedEventArgs> ClientDataReceived;

/// <summary>
/// Данные, полученные со стороны клиента, а также отправляемые со стороны сервера клиенту
/// </summary>
private ClientDataReceivedEventArgs Arguments = null;
#endregion

#region fields & properties

/// <summary>
/// ViewState ключи
/// </summary>
private string SuccessfullClientHandlerViewStateID
{
get
{
return this.ID + "_SuccessfullClientHandler";
}
}

/// <summary>
/// ViewState ключи
/// </summary>
private string ErrorClientHandlerViewStateID
{
get
{
return this.ID + "_ErrorClientHandler";
}
}

/// <summary>
/// ViewState ключи
/// </summary>
private string ServerCallFunctionNameViewStateID
{
get
{
return this.ID + "_ServerCallFunctionName";
}
}

/// <summary>
/// Имя сценария, который будет вызван в случае успешного обращения на сервер
/// </summary>
[Browsable(true)]
[Category("Callback Handlers")]
[DefaultValue("onSuccessfullClientHandler")]
[Description("Имя сценария, который будет вызван в случае успешного обращения на сервер")]
public string OnSuccessfullClientHandler
{
get
{
if (ViewState[SuccessfullClientHandlerViewStateID] != null)
return (String)ViewState[SuccessfullClientHandlerViewStateID];
else
return "";
}
set
{
ViewState[SuccessfullClientHandlerViewStateID] = value;
}
}

/// <summary>
/// Имя сценария, который будет вызван в случае неудачного обращения на сервер
/// </summary>
[Browsable(true)]
[Category("Callback Handlers")]
[DefaultValue("onErrorClientHandler")]
[Description("Имя сценария, который будет вызван в случае неудачного обращения на сервер")]
public string OnErrorClientHandler
{
get
{
if (ViewState[ErrorClientHandlerViewStateID] != null)
return (String)ViewState[ErrorClientHandlerViewStateID];
else
return "";
}
set
{
ViewState[ErrorClientHandlerViewStateID] = value;
}
}

/// <summary>
/// Имя функции, которая будет использоваться для вызова серверного сценария со стороны клиента, принимает один аргумент ARG в виде строки
/// </summary>
[Browsable(true)]
[Category("Callback Handlers")]
[DefaultValue("serverCall")]
[Description("Имя функции, которая будет использоваться для вызова серверного сценария со стороны клиента, принимает один аргумент ARG в виде строки ")]
public string ServerCallFunctionName
{
get
{
if (ViewState[ServerCallFunctionNameViewStateID] != null)
return (String)ViewState[ServerCallFunctionNameViewStateID];
else
return "";
}
set
{
ViewState[ServerCallFunctionNameViewStateID] = value;
}
}
#endregion

protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
string webFormDoCallbackScript = this.Page.ClientScript.GetCallbackEventReference(this, "arg", this.OnSuccessfullClientHandler, null, true);
string serverCallScript = "function " + this.ServerCallFunctionName + "(arg){" + webFormDoCallbackScript + ";\n}\n";

if (!this.Page.ClientScript.IsClientScriptBlockRegistered(this.ServerCallFunctionName))
{
this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(), this.ServerCallFunctionName, serverCallScript, true);
}
}

protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);

}

#region ICallbackEventHandler Members

public string GetCallbackResult()
{
// прошла обработка данных в Argumets на стороне сервера, отправляем результат на клиента
if (this.Arguments != null)
{
return this.Arguments.ServerResponse;
}
else
return "";
}

public void RaiseCallbackEvent(string eventArgument)
{
this.OnClientArgumentReceived(eventArgument);
}

#endregion

#region event handlers

public virtual void OnClientArgumentReceived(string eventArgument)
{
if (this.ClientDataReceived != null)
{
this.Arguments = new ClientDataReceivedEventArgs
{
ClientData = eventArgument
};

this.ClientDataReceived(this, this.Arguments);
}

}
#endregion

}
}


Обратите внимание на реализацию интерфейса ICallbackEventHandler, смысл которого был описан ранее в статье Как выполнить callback со стороны клиента на сервер в ASP.NET


Обращаем также внимание на код обработчика callbackHandler_ClientDataReceived в коде нашего code-behind класса страницы,
который был декларативно указан в разметке страницы в обработке контрола в строке
OnClientDataReceived="callbackHandler_ClientDataReceived".


protected void callbackHandler_ClientDataReceived(object sender, CallbackHandlerControl.Controls.ClientDataReceivedEventArgs e)
{
string clientData = e.ClientData;
e.Cancel = false;
e.ServerResponse = DateTime.Now.ToString();
}


Именно в нем мы получаем данные со стороны клиента, и подготавлиаем данные, отправляемые на клиента со стороны сервера



Данный обработчик принимает аргумент класса-наследника EventArgs - ClientDataReceivedEventArgs, которое содержит как данные, полученные со стороны клиента, так и данные, которые
будут отправлены клиентской стороне в качестве ответа от сервера. Нужно помнить также о том, что мы всегда можем отправить как со стороны сервера, так и со стороны клиента сложные объекты, использую сериализацию в строку -
потому вполне достаточно ограничиться строковоыми типами данных.





Само событие в коде нашего контрола выглядит следущим образом:

/// <summary>
/// Данное событие будет подниматься в случае получения данных со стороны клиента
/// на сервеной стороне в контроле <see cref="WebSite.WebModules.Common.Callbacks.CallbackEventHandler"/>
/// </summary>
[Browsable(true)]
[Category("Callback Handlers")]
[Description("Данное событие будет подниматься в случае получения данных со стороны клиента на сервеной стороне в контроле WebSite.WebModules.Common.Callbacks.CallbackEventHandler")]
public event EventHandler<ClientDataReceivedEventArgs> ClientDataReceived;




Как видите, обработчик события ClientDataReceived должен принимать аргумент класса ClientDataReceivedEventArgs, код которого представлен ниже:



using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace CallbackHandlerControl.Controls
{
/// <summary>
/// Параметры события ClientDataReceivedEvent, которое будет подниматься в случае получения данных со стороны клиента
/// на сервеной стороне в контроле <see cref="WebSite.WebModules.Common.Callbacks.CallbackEventHandler"/>
/// </summary>
public class ClientDataReceivedEventArgs : EventArgs
{

/// <summary>
/// Строка, отправленная со стороны клиента на сервер - это может быть как простой аргумент, так и
/// серилиазованное состояние объекта
/// </summary>
public string ClientData = "";

/// <summary>
/// Ответ от сервера, отправляемый на клиента - это может быть сериализованное состояние объекта или просто строка;
/// Логика по обработке данной строки лежит на стороне клиента
/// </summary>
public string ServerResponse = "";

/// <summary>
/// Данный флаг определяет, завершить цепочку выполнения Callback или нет
/// </summary>
public bool Cancel = false;
}
}




Ну и пара скриншотов:

результат выполнения callback по клику на кнопке на странице, разметка которого приведена в начале страницы:






Контрол в режиме редактирования на странице





Удачи в использовании.

ps: возможно, получилось сумбурно, но мне кажется, разобравшись с принципом проведения Callback-запроса третьим описанным способом из статьи Как выполнить callback со стороны клиента на сервер в ASP.NET
код (доступен по ссылке в начале статьи ) и принцип работы самого контрола становится понятным.

пятница, января 30, 2009

Specified argument was out of the range of valid values. Parameter name: utcDate

В очередной раз выкладывая на сервер приложение, заметил, что ajax-овый tab container control не загружает свои сборки..т.е. просто пропали стили обрамления самих вкладок, их подсветка при onmouseover, т.е. собственно то, что раньше отдавал ScriptResource.axd

Фидлером увидел 500 ошибку на обращение к обработчику ресурсов:


Specified argument was out of the range of valid values.
Parameter name: utcDate
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values.
Parameter name: utcDate

Source Error:

An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below.

Stack Trace:

[ArgumentOutOfRangeException: Specified argument was out of the range of valid values.
Parameter name: utcDate]
System.Web.HttpCachePolicy.UtcSetLastModified(DateTime utcDate) +3261043
System.Web.HttpCachePolicy.SetLastModified(DateTime date) +47
System.Web.Handlers.ScriptResourceHandler.PrepareResponseCache(HttpResponse response, Assembly assembly) +194
System.Web.Handlers.ScriptResourceHandler.ProcessRequest(HttpContext context) +1154
System.Web.Handlers.ScriptResourceHandler.System.Web.IHttpHandler.ProcessRequest(HttpContext context) +4
System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +154
System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +64





этот эффект происходит изза эффекта "assemplies in the future" :)
т.е. из-за разницы в таймзонах у меня сборка ушла на сервер с датой поздней, чем серверное время и обработчик ресурсов для ajax сходит с ума

Как решение, просто откомпилить сборку с датой ранее даты серверного времени;

пятница, января 23, 2009

Динамическое создание вкладок для ajax tab container

Динамическое создание вкладок для ajax tab container;
Ситуация была следующая - в разметке был размещен контейнер, для которого создавал вкладки динамически...
разметка:

<ajaxtoolkit:tabcontainer id="tabContainerCompanyProfile" runat="server" enableviewstate="true" width="100%">
</ajaxtoolkit:tabcontainer>


до постбека все происходило нормально, при переходе на другую страницу или при постбеке получал сообщение:

System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values.

ну и stack trace до вызова set_ActiveTabIndex;

лечится только динамическим созданием самого tab container как показано ниже:
помещаем в place holder и затем уже добавляем вкладки...


разметка:


<table>
<tr>
<td>
<asp:PlaceHolder ID="phTabContainer" runat="server" EnableViewState="true">
</asp:PlaceHolder>
</td>
</tr>
...........................


фрагмент кода:


var availableTabs = (from pd in this.ProfileDescriptors
where pd.CompanyTypes.Contains(this.CompanyType) &&
(pd.EditMode == EditMode.All || pd.EditMode == this.ProfileMode)
select pd).ToList();


if (availableTabs != null &&
availableTabs.Count > 0)
{

AjaxControlToolkit.TabContainer tabContainer = new AjaxControlToolkit.TabContainer();
tabContainer.Width = Unit.Parse("100%");
tabContainer.Enabled = true;
tabContainer.Visible = true;

phTabContainer.Controls.Add(tabContainer);

foreach (ProfileDescriptor profileDescriptor in availableTabs)
{
foreach (TabDescriptor tabDescriptor in profileDescriptor.TabDescriptors)
{
CompanyProfileTabs.CompanyPropertiesTabBase propertiesEditor = (CompanyProfileTabs.CompanyPropertiesTabBase)this.LoadControl(tabDescriptor.TabControlURL);
if (propertiesEditor != null)
{
AjaxControlToolkit.TabPanel tab = new AjaxControlToolkit.TabPanel();
tab.Visible = true;
tab.Width = Unit.Percentage(100);
tab.Enabled = true;
tab.HeaderText = tabDescriptor.HeaderCaption;

propertiesEditor.Visible = true;
tab.Controls.Add(propertiesEditor);


tabContainer.Tabs.Add(tab);

this.Tabs.Add(propertiesEditor);
}
}

}

if (this.Tabs.Count > 0)
{
tabContainer.ActiveTabIndex = 0;
}
}

вторник, ноября 25, 2008

JSON в ASP.NET

исходные коды
В
предыдущей теме
Callback in ASP.NET я рассматривал, как выполнить callback со стороны клиента на сервер, так сказать, сходить за данными. Здесь я рассказываю, как можно выполнить сериализацию объекта некоего класса со стороны сервера на клиента и использовать его в гораздо более удобной и в ООП-ориентированной форме;



В рассматриваемом примере на странице создается список объектов класса Customer, декларация класса следующая:



[Serializable]
public class CustomerInfo
{
private string firstName;
public string FirstName
{
get
{
return firstName;
}
set
{
if (firstName == value)
return;
firstName = value;
}
}


private string lastName;
public string LastName
{
get
{
return lastName;
}
set
{
if (lastName == value)
return;
lastName = value;
}
}

private string company;
public string Company
{
get
{
return company;
}
set
{
if (company == value)
return;
company = value;
}
}

[NonSerialized]
private string testNonSerialized = "NonSerialized string";

}



мы пометили класс как сериализуемый атрибутом Serializable, все поля простых типов данных данного класса могут быть сериализованы, за исключением тех полей, которые мы можем отметить атрибутом NonSerialized (забегая на будущее, иногда в имеющемся классе, который мы хотим использовать на стороне клиента, могут использоваться типы данных и поля, которые мы или просто не сможем использовать на стороне клиента или же нам просто данные поля не нужны)




Заполнение данными происходит следующим образом:

///
/// databinding & controls initialization
///

private void LoadData()
{
// db request and customers collection initialization
customers.Add(new CustomerInfo
{
FirstName = "John",
LastName = "Smith",
Company = "ITS"
});

customers.Add(new CustomerInfo
{
FirstName = "Alise",
LastName = "Young",
Company = "IT Research"
});

customers.Add(new CustomerInfo
{
FirstName = "Alex",
LastName = "Gullini",
Company = "AD Analitics"
});
}



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



Внешний вид страницы





Сама разметка страницы следующая:


<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="JSON._Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title></title>
<style type="text/css">
td { border:solid 1px #eaeaea; }
</style>

<script language="javascript" type="text/javascript">
var customerID = 0;

function onClickHandler() {

serverCall('get,' + customerID);
customerID++;
if (customerID > 2) {
customerID = 0;
}
}

function onSuccessfullHandler(responseFromServer) {
var customer = Sys.Serialization.JavaScriptSerializer.deserialize(responseFromServer);
if (customer != null) {
// binding
var txtFirstName = $get('txtFirstName');
var txtLastName = $get('txtLastName');
var txtCompany = $get('txtCompany');

txtFirstName.value = customer.firstName;
txtLastName.value = customer.lastName;
txtCompany.value = customer.company;
}
else
customerID = 0;
}

</script>
</head>
<body>
<form id="frmMain" runat="server">
<asp:ScriptManager ID="scriptManager" runat="server"></asp:ScriptManager>
<div>
<table style="border:solid 1px grey;">
<tr>
<td>
First name:
</td>
<td>
<input type="text" id="txtFirstName" />
</td>
</tr>

<tr>
<td>
Last name:
</td>
<td>
<input type="text" id="txtLastName" />
</td>
</tr>

<tr>
<td>
Company:
</td>
<td>
<input type="text" id="txtCompany" />
</td>
</tr>

<tr>
<td colspan="2">
<input type="button" id="btnGetNextCustomer" onclick="onClickHandler();" value="Next customer"/>
</td>
</tr>
</table>
</div>
</form>
</body>
</html>





Как видим, на странице используются обычные html-контролы, при нажатии на кнопку btnGetNextCustomer вызывается функция onClickHandler, которая и выполняет всю работу;


onClickHandler делает вызов функции serverCall, которую мы зарегестрировали в обработчике страницы Page_Load так как было рассказано в предыдущем моем топике, в частности, в той ее части, которая касается реализации интерфейса ICallbackEventHandler;



Страница работает следующим образом - на стороне клиента объявлена переменная customerID - это простой порядковый номер, которые передается в функцию обратного вызова serverCall, и затем этот номер увеличивается для запроса следующей записи и так далее по кругу - при достижении максимальной величины (3) он сбрасывается обратно в ноль;

При получении значения на стороне сервера в методе RaiseCallbackEvent(string eventArgument) мы сохраняем запрошенный номер во внутреннем поле currentCustomerID:




public void RaiseCallbackEvent(string eventArgument)
{
string[] argsFromClient = eventArgument.Split(',');

if (argsFromClient.Length >= 2)
{
switch (argsFromClient[0])
{
case "get":
int customerID;
if (int.TryParse(argsFromClient[1], out customerID))
{
this.currentCustomerID = customerID;
}

break;
default:
break;
}
}
}


Затем, при формировании ответа клиенту, мы сериализуем запрошенный объект Customer следующим образом:



public string GetCallbackResult()
{
string result = "{}";

if (this.currentCustomerID < this.Customers.Count)
{
CustomerInfo customer = this.Customers[this.currentCustomerID];
DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(CustomerInfo));

using (MemoryStream ms = new MemoryStream())
{
ser.WriteObject(ms, customer);
result = Encoding.Default.GetString(ms.ToArray());
}
}

return result;
}


Используем класс DataContractJsonSerializer, который объявлен в пространстве имен System.Runtime.Serialization.Json и находится в сборке System.ServiceModel.Web.dll - ее нужно подключить к проекту;


Результат сериализации в строку result вы можете увидеть на следующем скриншоте - по сути дела, данная строка представляет обычное отображение javascript-объекта в строку, если мы вызываетм метод toString() - сериализация объекта в javascript достаточно простая, сохраняются просто пары ключ-значение, что логично, так как любой класс в javascript - это просто словарь.

Ответ со стороны сервера






Далее, сериализованная строка уходит на клиента, где обрабатывается в нашем сallback обработчике onSuccessfullHandler следующим образом:

function onSuccessfullHandler(responseFromServer) {
var customer = Sys.Serialization.JavaScriptSerializer.deserialize(responseFromServer);
if (customer != null) {
// binding
var txtFirstName = $get('txtFirstName');
var txtLastName = $get('txtLastName');
var txtCompany = $get('txtCompany');

txtFirstName.value = customer.firstName;
txtLastName.value = customer.lastName;
txtCompany.value = customer.company;
}
else
customerID = 0;
}





Ответ со стороны сервера




В данном случае мы используем клиентский класс Sys.Serialization.JavaScriptSerializer и его метод deserialize из библиотеки ajax - обратите внимание, для того, чтобы классы из пространства имен Sys и прочих были доступны, на странице должен быть доступен ScriptManager. Также обратите внимание, что структура самого десериализованного объекта customer полностью совпадает с его серверным описанием, таким образом, мы можем использовать объявления ранее private полей из серверного объявления класса за исключением тех, которые были помечены атрибутом NonSerialized;



Десериализованный объект customer на клиенте






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


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

понедельник, ноября 24, 2008

Как выполнить callback со стороны клиента на сервер в ASP.NET

исходные коды
Н
е рассматривая update panel и не используя закрытые методы класса PageRequestManager из библиотеки ajax, я выделяю для себя способы:

  1. Page methods


    Тут все достаточно просто; достаточно реализовать в web-форме статический серверный метод страницы, пометить его атрибутом WebMethodAttribute, разместить в разметке ScriptManager control, установить у него свойство EnablePageMethods и требуемый метод будет доступен через сгенерированный на стороне клиента объект PageMethods;



    В прилагаемом примере в странице PageMethods.aspx я реализовал следующий метод:




    [WebMethod(true)]
    public static string PageMethodTest(string arg)
    {
    return "this is a string from server";
    }


    На стороне клиента разместил ScriptManager следующим образом:


    <asp:scriptmanager runat="server" id="scriptManager" enablepagemethods="true">

    </asp:scriptmanager>

    И зарегестрировал серверную кнопку:



    <asp:button id="btnPageMethodInvocation"
    runat="server"
    text="Page method invocation"
    onclientclick="btnPageMethodInvocation_clientHandler();return false;">

    </asp:button>

    При клике по кнопке будет выполнен клиентский сценарий btnPageMethodInvocation_clientHandler, после чего будет возврат значения false, что говорит о необходимости разовать цепочку вызовов клиентских сценариев, одним из которыхъ является возврат формы (собственно сам postback), а именно его мы и хотим избежать; Таким образом, при клике на форму у нас должен выполниться только сценарий btnPageMethodInvocation_clientHandler;


    Сам сценарий будет следующим:

    function btnPageMethodInvocation_clientHandler() {
    PageMethods.PageMethodTest("test", onSuccessfullHandler);
    }










    Как видите, происходит обращение к глобальному клиенскому объекту PageMethods, и вызывается метод, который мы описали на серверной стороне; Сигнатура данного клиентского метода будет отличаться только наличием дополнительных параметров, которые указывают на javascript функции успешного и неуспешного вызова серверного метода, для того чтобы собственно говоря и реализовать сам принцип асинхронности – сделав вызов PageMethods.PageMethodTest, выполнение сценария не прерывается, а результат будет передан в onSuccessfullHandler функцию с единственным параметром result в случае успешного выполнение callback, как показано ниже:


    function onSuccessfullHandler(result) {
    alert(result);
    }


    Сигнатура самого метода следующая:

    Сигнатура метода


    Здесь я просто показываю значение параметра result, значение которого мы получаем со стороны сервера;


    Иными словами, все выполнение происходит следующим образом:
    ScriptManager, проанализировав codebehind класс страницы и обнаружив статик метод с атрибутом WebMethod, сгенерировал объект PageMethods и соотвествующий по сигнатуре клиенский метод, которые, собственно и реализует асинхронный вызов одноименного серверного метода, передав значение со стороны клиента; Серверный метод делает возврат значения (в нашем примере мы возвращаем простой тип данных, о том как делать возврат сложных типов данных с применением json сериализации/десериализации я расскажу в следующем посте), которое в процессе выполнения callback будет передано в клиентский сценарий, который мы указали onSuccessfullHandler;

    Подводя итог, цепочка вызовов будет следующей:

    function btnPageMethodInvocation_clientHandler()
    PageMethods.PageMethodTest("test", onSuccessfullHandler)
    public static string PageMethodTest(string arg)
    function onSuccessfullHandler(result)


    Результат вызова нашей страницы будет слеюущим:


    Результат вызова



    Данный вариант реализации асинхронного вызова очень удобен на мой взгляд, если бы не огромный минус – а именно, невозможность его использования в User-контролах;

  2. Script Service


    Второй вариант лишен описанного выше минуса; Смысл заключается в следующем – реализуется web-сервис, code behind класс сервиса помечается атрибутом ScriptService, а методы, которые мы хотим использовать на стороне клиента – атрибутом ScriptMethod; В разметке самой страницы обязательно присуствие ScriptManager с указанием того ScriptService, методы которого мы хотим получить и использовать на стороне клиента;



    В нашем случае я сделал следующий web-service:



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

    [WebMethod]
    [ScriptMethod]
    public string ScriptServiceTest(string arg)
    {
    return "this is a string from script service method";
    }
    }



    Создал страницу ScriptServiceCallPage.aspx, код разметки которой выглядит следующим образом:



    <title></title>
    <script language="javascript" type="text/javascript"
    function btnPageMethodInvocation_clientHandler() {
    Callbacks.ScriptServices.ScriptService.ScriptServiceTest("test",
    onSuccessfullHandler);
    }

    function onSuccessfullHandler(result) {
    alert(result);
    }
    </script>


    <form id="form1" runat="server">
    <asp:scriptmanager runat="server" id="scriptManager" enablepagemethods="true">
    <services>
    <asp:servicereference path="~/ScriptServices/ScriptService.asmx">
    </asp:servicereference>
    </services>
    <div>
    <asp:button id="btnPageMethodInvocation"
    runat="server" text="Script service method invocation"
    onclientclick="btnPageMethodInvocation_clientHandler();
    return false;">
    </asp:button></div>
    </asp:scriptmanager>



    </form>




    Отличие от предыдущего примера с использованием сгенерированного объекта PageMethods в том, что мы указываем конкретный ScriptService в ScriptManager, который генерирует соотвествующий javascript класс с методами, сигнатура который соотвествует серверным методам + добавляет параметры для указания javascript функций в случае успешного или неудачного выполнения асинхронного вызова; Сгенерированный объект, который мы можем использовать на стороне клиента, будет иметь имя, полностью совпадающего с указанным script service, т.е. включая c# namespace и имя класса – в нашем случае это Callbacks.ScriptServices.ScriptService; В остальном принцип работы с script service ничем не отличается от PageMethods; Достоинство данного способа состоит в том, что часто используемые операции, вызов которых требуется на клиенте, сосредоточены в одном месте, и доступны как из классов форм, так и user controls;






  3. Третий способ, который лично мне больше всего нравится и на мой взгляд наиболее гибок и прозрачнее для меня, является явная реализация интерфейса IcallbackEventHandler user контролом или классом страницы; Данный способ также описан у Дино Эспозито, автора множества книг-бестселлеров по технологиям AJAX и ASP.NET;



    В нашем примере я создал страницу ICallbackEventHandlerImplementor.aspx, у code behind класса реализовал интерфейс ICallbackEventHandler как показано ниже:


    public partial class ICallbackEventHandlerImplementor : System.Web.UI.Page, ICallbackEventHandler
    {
    #region ICallbackEventHandler Members

    public string GetCallbackResult()
    {
    string resultFromServer = "this is a string from ICallbackEventHandler implementor";
    return resultFromServer;

    }

    public void RaiseCallbackEvent(string eventArgument)
    {
    string[] argsFromClient = eventArgument.Split(',');

    if (argsFromClient.Length >= 2)
    {
    switch (argsFromClient[0])
    {
    case "add":
    break;
    default:
    break;
    }
    }
    }

    #endregion
    }


    Метод RaiseCallbackEvent будет вызываться со стороны клиентаи принимаеть едиственный строковый параметр, значение которого мы сформируем на стороне клиента; Метод GetCallbackResult будет использоваться для формирования результата, который мы вернем обратно клиентской стороне;

    Итак, что же будет являться началом асинхронной операции – а началом асинхронной операции со стороны клиента будет являться вызов функции WebForm_DoCallback, сигнатура да и собственно название которого не стоит запоминать и использовать напрямую, так они могуто измениться в следующих версиях; Вместо этого сам код для вызова мы можем получить, используя класс ClientScriptManager через свойство ClientScript текущей страницы, используй метод GetCallbackEventReference; Данный метод генерирует вызов метода WebForm_DoCallback на стороне клиента, который мы можем использовать в нашем клиентском сценарии; В нашем примере я хочу сгенерировать клиенский сценарий serverCall, который бы принимал единственное строковое значение, отправлял бы его на выполнение серверу и собственно на этом бы его функции бы заканчивались:



    Т.е. что-то типа такого:


    function serverCall(arg)
    {
    WebForm_DoCallback(…arg)
    }


    Как это сделать, показано в реализации обработчика Page_Load нашей рассматриваемой страницы:

    protected void Page_Load(object sender, EventArgs e)
    {
    if (!IsPostBack)
    {
    string webFormDoCallbackScript = this.ClientScript.GetCallbackEventReference(this, "arg", "onSuccessfullHandler", null, true);
    string serverCallScript = "function serverCall(arg){" + webFormDoCallbackScript + ";\n}\n";

    if (!this.ClientScript.IsClientScriptBlockRegistered("serverCallScript"))
    {
    this.ClientScript.RegisterClientScriptBlock(this.GetType(), "serverCallScript", serverCallScript, true);
    }

    }
    }






    Что здесь происходит? Сначала я получаю в виде строки сам вызов WebForm_DoCallback, причем указываю, как будет называться его единственный аргумент, а также имя той javascript функции, в которую будет передан результат со стороны сервера по завершении асинхронного вызова; затем я формирую функцию враппер наз функцией асинхронного вызова – а именно нашу функцию serverCall – после запуска страницы в сгенерированном исходном тексте вы увидите следующий скрипт:




    <script type="text/javascript">
    //<![CDATA[
    function serverCall(arg){WebForm_DoCallback('__Page',arg,onSuccessfullHandler,null,null,true);
    }
    //]]>
    </script>



    Теперь же можно использовать нашу функцию-враппер serverCall со стороны клиента, как показано в разметке страницы:




    <head id="Head1" runat="server">
    <title></title>
    <script language="javascript" type="text/javascript">
    function btnPageMethodInvocation_clientHandler() {
    serverCall('add,' + 10);
    }

    function onSuccessfullHandler(result) {
    alert(result);
    }
    </script>
    </head>
    <body>

    <form id="form1" runat="server">
    <div>
    <asp:Button ID="btnPageMethodInvocation"
    runat="server"
    Text="ICallbackEventHandlerImplementor sample"
    OnClientClick="btnPageMethodInvocation_clientHandler();return false;" />
    </div>
    </form>
    </body>
    </html>


    Сама разметка, как видите, практически не отличается от предыдущих страниц, за исключением того, что здесь не требуется ScriptManager; Ограничение в количестве параметров, передаваемых в функцию асинхронного вызова WebForm_DoCallback, не ограничивает однако нас от формирования такой строки, значение которой мы можем рассматривать как несколько параметров, что собственно и продемонстрировано в примере:


    На клиенте мы передаем serverCall('add,' + 10); строку “Add, 10”, на стороне сервера делаем сплит строки и дальше уже анализируем полученные значения для выполнения какого либо действия на стороне сервера незаметно от клиента;

    Лично мне последний способ нравится больше в виду того, что я получаю больший контроль над самим асинхронным вызовом, чем в первых двух описанных;



    ps: Существуют и другие способы асинхронных вызовов, например,в том же самом DevExpress есть даже специальный серверный контрол, в котором указывается javascript handler на получение результата со стороны сервера и серверный обработчик, который и выполняет анализ данных со стороны клиента;









пятница, июля 18, 2008

Перестала работать UpdatePanel :)

Что удивительно, она перестала работать :) то есть все тригерные контролы стали приводить не просто к перегрузке содержимого updatePanel, а к полной перегрузке страницы, т.е. чего хотелось избежать ( именно постбеков) так и не получилось...

причем в процессе отладки было видно, что серверный код обработчика вызывается, но на стороне клиента происходит исключение вида (все таки отладка на стороне клиентского js великая сила :)) -

Sys.WebForms.PageRequestManagerParserErrorException: The message received from the server could not be parsed. Common causes for this error are when the response is modified by calls to Response.Write(), response filters, HttpModules, or server trace is enabled.

т.е. судя по всему, тот код который отдавался в процессе калбека со стороны сервера клиенту в onSuccessHandler не мог быть распарсен, что приводило к тому, что updatepanel не могла перерисовать свое содержимое, и наверно самое верное решение со стороны ajax client runtime было обновить всю страницу ..


на самом деле, сразу было очевидно то, что дело было в движке проекта или в сайте, в работе самого ajaxToolkit script manager сомневаться не приходилось, так как на отдельном простеньком сайте скрипт менеджер и update panel вели себя так как и положено;

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

http://weblogs.asp.net/leftslipper/archive/2007/02/26/sys-webforms-pagerequestmanagerparsererrorexception-what-it-is-and-how-to-avoid-it.aspx

а именно:
вырезка

Why do I keeping getting a PageRequestManagerParserErrorException?

Well, chances are you're doing one of the things mentioned in the error message. Here are the most common reasons and why they don't work:

1.
Calls to Response.Write():
By calling Response.Write() directly you are bypassing the normal rendering mechanism of ASP.NET controls. The bits you write are going straight out to the client without further processing (well, mostly...). This means that UpdatePanel can't encode the data in its special format.
2.
Response filters:
Similar to Response.Write(), response filters can change the rendering in such a way that the UpdatePanel won't know.
3.
HttpModules:
Again, the same deal as Response.Write() and response filters.
4.
Server trace is enabled:
If I were going to implement trace again, I'd do it differently. Trace is effectively written out using Response.Write(), and as such messes up the special format that we use for UpdatePanel.
5.
Calls to Server.Transfer():
Unfortunately, there's no way to detect that Server.Transfer() was called. This means that UpdatePanel can't do anything intelligent when someone calls Server.Transfer(). The response sent back to the client is the HTML markup from the page to which you transferred. Since its HTML and not the special format, it can't be parsed, and you get the error.

How do I avoid getting a PageRequestManagerParserErrorException?

To start with, don't do anything from the preceding list! Here's a matching list of how to avoid a given error (when possible):

1.
Calls to Response.Write():
Place an or similar control on your page and set its Text property. The added benefit is that your pages will be valid HTML. When using Response.Write() you typically end up with pages that contain invalid markup.
2.
Response filters:
The fix might just be to not use the filter. They're not used very often anyway. If possible, filter things at the control level and not at the response level.
3.
HttpModules:
Same as response filters.
4.
Server trace is enabled:
Use some other form of tracing, such as writing to a log file, the Windows event log, or a custom mechanism.
5.
Calls to Server.Transfer():
I'm not really sure why people use Server.Transfer() at all. Perhaps it's a legacy thing from Classic ASP. I'd suggest using Response.Redirect() with query string parameters or cross-page posting.



в сущности все сводится к тому, чтобы избежать ручного responce.write(); вспомнив, что в проекте используется дополнительный httpModule, который выводит отладочную информацию и который собственно говоря и осуществляет write(), пришлось его отключить в пользу нормальной работы script manager & update panel, что и позволило избежать вышеописанной ошибки...


четверг, июля 17, 2008

Ajax: The system cannot locate the resource specified;

При отладке ajax вдруг стала происходить странная ошибка в коде вызова ScriptMethod-а отдельно выделенного ScriptService,
в частности я дебажил некоторый модуль .ascx, в котором происходит обращение к объекту-прокси, созданному на стороне клиента:
код на стороне клиента js:

Sys.Debug.fail('');
if (typeof(siStatuses) != "undefined" && siStatuses != null)
{
WS.Scripts.ScriptService.CheckStockImagesDownloading(siStatuses, OnCheckComplete, OnCheckFailed);
}


при обращении происходил exception на стороне клиента вида:
The system cannot locate the resource specified;

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

Sys.Net.WebServiceProxy.invoke(servicePath, methodName, useGet, params, onSuccess, onFailure, userContext, this.get_timeout());

........
this._xmlHttpRequest.send(body);



путем логического осмысления :) пришел к выводу что длина сообщения, передаваемого в scriptMethod была
значительно больше чем килобайт, указанный собственно говоря через атрибут UseHttpGet:


[ScriptMethod(ResponseFormat = ResponseFormat.Json, UseHttpGet = true)]


гораздо больше...:)) ну и соотвественно напрашивается вывод (чисто логически потому что более менее внятного сообщения об ошибке от ie я не смог получить, или просто не смог докопаться до самых недр) что xmlHttpRequest object просто не может совершить
такое обращение, путем перестановки на


[ScriptMethod(ResponseFormat = ResponseFormat.Json, UseHttpGet = false)]




т.е. мы разрещаем передачу сообщения только методом post, что значительно расширяет возможности самой передачи и снимает ограничение на длину сообщения, что собственно и лечит данную злокачественную опухоль... вылечена; :)