понедельник, ноября 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 на получение результата со стороны сервера и серверный обработчик, который и выполняет анализ данных со стороны клиента;









6 комментариев:

Michael Smirnov комментирует...

Мне тоже последний метод понравился больше всего - не надо пложить веб-сервисы и можно использовать не только на страницах, но и в контролах.

Но, честно говоря, немного отпугнуло ограничение на передачу только одного параметра - мне кажется парсить строку как-то не очень наглядно........

Андрей Маркеев комментирует...

одна строка - это нормально, с тех пор как существует json. сериализуй туда чо душе угодно

Tim комментирует...

Можете подсказать почему у меня 3 вариант не работает в DotNetNuke? Почему то не срабатывает функция которая возвращает значение на сервер!

Tim комментирует...

Можете подсказать почему у меня 3 вариант не работает в DotNetNuke? Почему то не срабатывает функция которая возвращает значение на сервер!

Vinyl комментирует...

У меня второй метод не рабоает. Бьюсь...Ошибка "Callbacks не определен"...

Andrey Smirnov комментирует...

Можете прислать кусочек вашего кода на guid.empty@gmail.com или прямо сюда определение сервиса и объявление aspx ascx?