Отказ от ответственности
Многое, что тут будет сказано может оказаться диким баяном и говнокодом. Кроме того, этот пост расчитан на достаточно озадаченных проблемами людьми, описанными в первом абзаце. Это не учебник, и не руководство к действию. Это только описание похода, и не более чем.
Классика создания контролов 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-а), индикацией результата валидации, да так, чтобы контрол был в отдельной сборке. Это несложный контрол, который покажет общую идею.
Как это делают другие?
Для начала рассмотрим несколько библиотек, аналогичных по функционалу, чтобы понять как они это делают:
- MVC Controls Toolkit использует методику возврата StringBuilder-а из хелперов HtmlHelper.
- MVCContrib project использует TagBuilder.
Другие коммерческие контролы в основном используют подход «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)); } }
Окей, пришло время написать код представления нашего контрола:
@* 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/
Добавить комментарий