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

—

от автора

Наверняка все создавали свои 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 (зеркало).

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

ссылка на оригинал статьи http://habrahabr.ru/post/165025/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *