Пользователь
0,0
рейтинг
8 января 2013 в 01:22

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

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

Комментарии привествуются.
Dmitry @jonie
карма
16,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (8)

  • +2
    По производительности стрингбилдер лучше всего… поэтому его и используют… и МС в своих веб-решениях тоже
    • –1
      На сколько в процентах StringBuilder быстрее, чем то что я показал (которое, к слову, внутренне его же использует)? Хочу цифры.
      • –1
        stackoverflow.com/questions/5234147/why-stringbuilder-when-there-is-string
        Кратко: При конкатенации строк через + каждый раз создается новый объект для каждой операции конкатенации.
        На SO есть тесты.
        • +1
          клево, а вас не смутило что по ссылке речь про java?) Кроме того ответа на свой вопрос я так и не увидел.
          И вообще как конкатенация строк связана с этим постом? Отходите от праздников-то уже…
          • –1
            support.microsoft.com/kb/306822/ Это от Microsoft. Ну а по поводу причин комента — это не ко мне.
            • –1
              ну вы там написали про конкатенацию строк — которую я так и не понял где увидели… Ладно, забудьте, про StringBuilder и его внутреннее устройство я, поверьте, вкурсе (как, уверен, 99% asp.net (да и C#) разработчиков).
  • +1
    Прям неделя MVC на Хабре. :)

    А вы про display template'ы не слышали? Зачем этот весь огород-то городить?

    Вот, умный мужик Скот Хенселман (Scott Hanselman), показал как «сложный» контрол создаётся в MVC

    ASP.NET MVC DisplayTemplate and EditorTemplates for Entity Framework DbGeography Spatial Types

    И всё работает просто замечательно, а от конструкций:

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


    N.B.
    Можно и крышкой отъехать, такое впечатление что вы пытаетесь портировать мышление User Control/Web Control'а из ASP.NET WebForms в MVC.
    • 0
      Вы украли мою мысль) Это чистый WebForms-подход — HTMLразметка плюс таскаемый за ней условный «codebehind».

      Хотя идея все-равно интересная.

      PS. мы для себя поняли, что проще всего кнтролы делать через Razor-helpers в папке App_Code. И razor-разметка есть, и код можно между @{...} засунуть… А чтобы использовать один контрол в нескольких приложениях мы юзаем саб-репозитарий git / hg который включен сразу в несколько проектов.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.