Простой путь создания сложных ASP.NET MVC контролов

в 21:22, , рубрики: .net, ASP, ASP.NET, asp.net mvc, asp.net mvc 4, control, метки: , , , ,

Наверняка все создавали свои asp.net mvc контролы (речь, конечно, про asp.net mvc кодеров). Вам должен быть знаком метод создания контролов, используя TagBuilder? Побывали писать реально сложные контролы (например с большим количеством javascript-та или разметки, которая зависит от опций)? Тогда наверняка вам знаком адЪ экранирования кавычек, конкатенации строк (или вызова .Format() функции) и т.п. «неудобства». Я предлагаю взглянуть на достаточно простую методику, которая позволит избежать подобных вещей и в тоже время сосредоточиться на функциональной стороне контролов, а не на программировании шелухи.

Отказ от ответственности

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

Классика создания контролов ASP.NET MVC

Кратенько пробежимся по тому как сейчас лепят контролы.
Обычно делают HtmlHelper extension вроде такого:

public static string InputExControl(this HtmlHelper @this)
{
  StringBuilder sb = new StringBuilder();
  sb.Append("<input type="text">.......blablabla..........");
  return sb.ToString();
}

либо используют TagBuilder под теже цели:

public static string InputExControl(this HtmlHelper @this)
{
  TagBuilder tagBuilder = new TagBuilder("input");
  //.....blablabla....
  return tagBuilder.ToString();

Лично меня от этого кода воротит: как минимум этот код сложно поддерживать (ИМХО), и уж тем-более модифицировать тоже не просто.

Задача

Давайте поставим такую задачу: нам надо сделать input контрол с AJAX валидацией на стороне сервера (на основе заданного Regexp-а), индикацией результата валидации, да так, чтобы контрол был в отдельной сборке. Это несложный контрол, который покажет общую идею.

Как это делают другие?

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

Другие коммерческие контролы в основном используют подход «StringBuilder.Format()» (для чистых asp.net mvc контролов), но могут (как например DevExpress) тянуть за собой asp.net webForms контролы.

Идея

Идея заключается в том, чтобы использовать Razor синтаксис asp.net mvc partial view для описания контрола в .cshtml, но при этом не тащить за собой .cshtml файлы, конечно же.

Создадим простой asp.net mvc 4 проект в студии, добавим к решению library (назовем его MyControlLib), в которой зареференсимся на основые asp.net mvc 4 либы.

Добавим в MyControlLib либу InputExControl.cshtml файл с пустым содержимым. Это будет наша View-часть контрола, которую мы ранее писали, используя TagBuilder методику. Для того чтобы перевести этот cshtml файл в C# код, мы будем использовать Razor Generator. Он позволит нам сгенерировать то, что Razor движек сгенерировал бы нам «на лету» в asp.net mvc приложении. Нужно это по сути для того чтобы не таскать за собой .cshtml файлы и казаться «взрослым контролом» (коммерческие же не тянут, вот и мы не станем). Окей, проставим в содержимом InputExControl.cshtml следующее:

@* Generator: MvcView *@

, а в его свойствах укажем Custom Tool как RazorGenerator.

Получили генерированный класс-наследник от System.Web.Mvc.WebViewPage. Генерация не очень удобная, т.к. создает не partial класс (можно исправить в исходниках генератора или просто руками в генерированном файле), т.е. такой класс тяжело расширять нужными нам методами.

Создадим класс InputExControlSettings, который будет олицетворять настройки нашего контрола. Он будет очень прост:

    public class InputExControlSettings
    {
        public string Name { get; set; }  //имя контрола

        public dynamic CallbackRouteValues { get; set; }  //значения для ajax callback-а

        public string ValidationRegexp { get; set; } //регулярка для проверки
    }

Для простоты я завел поле Name, которое будет олицетворять в клиентском коде контрола его id (свойство DOM элемента). Также это поле будет участвовать в генерации имён субэлементов, нужных нашему контролу (индикатор результата валидации).

Свойство CallbackRouteValues будет нужно нам для получения Uri куда мы из клиентского javascript зашлем запрос на валидацию. В нём обычно указывают контроллер и метод контроллера.

Теперь класс настроек можно указать в качестве модели для нашего контрол-cshtml файла:

@* Generator: MvcView *@
@model InputExControlSettings

@{
//ну и сразу определим пару переменных, чтобы ссылаться на них в коде
    string controlId = Model.Name;
    string controlResultId = Model.Name + "_Result";
}

Для того чтобы написать код дальше надо понять одну простую вещь: callback будет дёргать в общем случае наш же контрол, посему нам надо как-то отличать callback от простого GET запроса для получения внешнего вида контрола. Простым методом определения для нас я выбрал наличие в хедерах запроса «специального» (нашего) значения. Т.о. у нас появился небольшой хелпер. Кроме того я использовал его же (bad code!) как помошник получения Uri из значений CallbackRouteValues :

 internal static class InputExControlHelper
    {
        public static bool IsCallback()
        {
            return !string.IsNullOrEmpty(HttpContext.Current.Request.Headers["InputExControAjaxRequest"]);
        }

        public static MvcHtmlString CallbackHeaderName
        {
            get { return MvcHtmlString.Create("InputExControAjaxRequest"); }
        }

        public static string GetUrl(dynamic routeValues)
        {
            if (HttpContext.Current == null) throw new InvalidOperationException("no context");

            RequestContext context;
            if (HttpContext.Current.Handler is MvcHandler)
            {
                context = ((MvcHandler) HttpContext.Current.Handler).RequestContext;
            }
            else
            {
                var httpContext = new HttpContextWrapper(HttpContext.Current);
                context = new RequestContext(httpContext, new RouteData());
            }

            var helper = new UrlHelper(context, RouteTable.Routes);
            return helper.RouteUrl(string.Empty, new RouteValueDictionary(routeValues));
        }
    }

Окей, пришло время написать код представления нашего контрола:

код cshtml представления

@* Generator: MvcView *@
@model InputExControlSettings

@{
    string controlId = Model.Name;
    string controlResultId = Model.Name + "_Result";
}

@if(!InputExControlHelper.IsCallback())
{
    <input type="text" id="@controlId"/>
    <span id="@controlResultId"></span>
    
    <script>
        $(function() {
            $('#@controlId').change(function () {
                $('#@controlResultId').text('validating ...');
                
                $.ajax({
                    url: '@InputExControlHelper.GetUrl(Model.CallbackRouteValues)',
                    headers: {
                        '@InputExControlHelper.CallbackHeaderName': true
                    },
                    cache: false,
                    data: { value: $('#@controlId').val() },
                    type: 'POST',
                    dataType: 'json',

                    success: function (data) {
                        if (data) {
                            $('#@controlResultId').text('Validattion result: ' + data.result);
                        } else {
                            alert('result error?');
                        }
                    },
                    error: function() {
                        alert('ajax error');
                    }
                });
                
            });
        });
    </script>
}
else
{
    System.Web.HttpContext.Current.Response.ContentType = "application/json";
    @(this.InternalValidate(System.Web.HttpContext.Current.Request.Form["value"]))
}

Код максимально прост: если у нас пришел НЕ callback, то выводим основной View, включая javascript, который и будет делать этот самый callback. В случае же каллбэка мы ставим ContentType как JSON и вызываем метод валидации контрола InternalValidate(string).

Собственно код самой валидации и установки ViewData.Model будет оформлен как partial метод InputExControl-а и будет очень прост:

partial class InputExControl
    {
        public InputExControl(InputExControlSettings settings)
        {
            ViewData.Model = settings;
        }

        private MvcHtmlString InternalValidate(string value)
        {
            Thread.Sleep(2000); //long validation emulator...

            var settings = ViewData.Model;
            var regexp = new Regex(settings.ValidationRegexp, RegexOptions.Compiled);
            var res = regexp.IsMatch(value);
            var scriptSerializer = new JavaScriptSerializer();
            var rv = scriptSerializer.Serialize(new { result = res });
            return MvcHtmlString.Create(rv);
        }
    }

Ок, мы написали контрол, но мы пока не можем использовать его в нашем MVC проекте. Настоящие пацаны пишут под такие контролы расширитель HtmlHelper-а, что мы и сделаем:

namespace MyControlLib
{
    public static class HtmlExtensions
    {
         public static HtmlString InputEx(this HtmlHelper @this, Action<InputExControlSettings> setupFn)
         {
             var options = new InputExControlSettings();  //создаем наши настройки
             setupFn(options);  //сетапим их

             var view = new InputExControl(options);  //наш супер контрол
             var  tempWriter = new StringWriter(CultureInfo.InvariantCulture);  //буфер куда будет писаться результат работы движка Razor
             
             view.PushContext(new WebPageContext(), tempWriter);  //ставим контекст движку
             view.Execute(); //выполняем наше View - код в сгенерированном файле
             view.PopContext(); // восстанавливаем контекст

             return MvcHtmlString.Create(tempWriter.GetStringBuilder().ToString());  //вернем результат в внешнее View
         }
    }
}

Оккей, у нас есть теперь метод -расширитель. Настало время интеграции нашего контрола в основное приложение. Просто создайте PartialView MyInputCtrlPartial (и Action method именованный также), где впишите нечто вроде

@using MyControlLib

@Html.InputEx(s=>
                  {
                      s.Name = "MyInputCtrl";
                      s.CallbackRouteValues = new { Controller = "Home", Action = "MyInputCtrlPartial" };
                      s.ValidationRegexp = @"^d+$";
                  })

и вызовете его (используя Html.Partial(«MyInputCtrlPartial»)) в основной View.

Нам нужено описать контрол именно в PartialView, т.к. результат «рендеринга» будет разный — в зависимости от переданного хедера-индикатора что у нас идёт callback на проверку.

Осталось только запустить проект на выполнение и убедится что всё работает (или не работает, т.к. кто-то накосячил) (note: чтобы вызвать событие changed надо тыкнуть мимо контрола мышкой).

Итог

Итог: мы смогли написать непростой контрол, при этом у нас работает Intellisense в Razor шаблоне (включая javascript), что не может не радовать.

Пример проекта можно скачать с http://rghost.ru/42818685 (зеркало).

Комментарии привествуются.

Автор: jonie

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js