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

Regex для распарсивания выражений с форматирующей маской {name: formatMask, formatLength}

Понадобилось недавно написать regex-выражение для распарсивания строк вида {name: formatMask, formatLength}, при этом форматирующая маска или длина могут присутствовать или отсутствовать.

Привожу код такого regex:


\{\s*(?<FieldExpression>\s*(?<FieldName>\w{1,}){1}\s*(?<FormatExpression>(\s*(?<ColonSeparator>[:])|(?<CommaSeparator>[,]))\s*(?(ColonSeparator)(?<FormatMask>\w*)|)[,]*\s?(?<Length>\d+)*)*)\s?\}


В группах имеем возможность получить сам fieldName, его маску fieldMask и длину length. Еще хочу отметить, что при работе с regex очень помогает такой инструмент как Rad Software Regular Expressions Designer -http://www.radsoftware.com.au/?from=RegexDesigner.



Пишем 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); }
 });

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