Как стать автором
Обновить

Комментарии 71

А не до фига ли классов чтобы вывести такие простые данные? Такое впечатление что подобные архитектуры реализуются только для того чтобы реализовать какие-то академические идеи описанные теоретиками оторвывшимися от реального программирования. Вот если пересчитать все упомянутые конструкции и задать простой наивный вопрос а какую задачу все это решает таким невообразимо индусским образом.
Нет, совсем не до фига. В реальном приложении будет раз в 10-20 больше свойств и разметки — а количество вспомогательных классов не увеличится.
Добрый вечер, Леонид. Простите, что простые примеры ввели в заблуждение. Разумеется, в реальном проекте они всегда сложнее. Тем не менее, идеи, описанные в данной статье, ни в коей степени не являются исключительно «академическими» — всё это, напротив, получено и придумано в непосредственном процессе разработки.
Предлагаемая архитектура позволяет масштабироваться проекту (и, при необходимости, сужаться) при малых затратах благодаря малой связности компонентов системы.
Действительно, это выглядит как оверхед на примере в статье, но ведь контент рассчитан на читателей, которые уже столкнулись с данными проблемами, и была надежда, что сложные примерны ни к чему. Удачного вам остатка дня!

Подскажите, зачем сейчас использовать Razor?
Вы не знали, что Javascript победил?
Да, все становится чуть сложнее, но такие задачи решаются куда меньшей кровью.

Здравствуйте! Если вы про то что будущее за SPA — быть может, вы и правы, однако, в статье данный вопрос не затрагивается. А до тех пор, пока есть MPA — есть и Razor (и другие server-side шаблонизаторы). Готов побеседовать на данную тему не рамках данного поста, а, например, в личной беседе.

А изоморфный рендеринг?

Вот как раз для него Razor точно не нужен…
> Javascript победил
Только в воображении сектантов. Клиентского рендеринга должен быть абсолютный минимум.

Сейчас сектант — это вы

Тем не менее, Razor, поправьте если не прав, не возможно динамически сгенерировать run time, т.е. сам шаблон. Это остановило развитие сервер сайд рендеринга ASP MVC (php и т.п. развивались вместе с развитием восхитительных, для меня, CMS). Метапрограммирование при помощи T4 и в compile time — это эрзац замена — сгенерировать что-то можно, но развивать вместе с генератором не получается.
А можно кейс генерации шаблона в рантайме?
Все кейсы где вы использовали T4 (для генерации Razor'а) в девтайм. Это не отмазка, просто вопрос так задан, что можно препдположить что в «девтайм» — кейсы вам понятны и ясны.
Но зачем его генерировать run time когда в нем самом можно взять и написать цикл по метаданным?

Ну и для «особо буйных» ничто не мешает реализовать IViewEngine
Не надо IViewEngine сейчас самому. ServiceStack сейчас пытаются продвинуть шаблонизатор под ASP с возможностью генерирования run time.

На вопрос «зачем его генерировать run time » — ответ такой же — затем же зачем в dev time… Если у вас нет потребности генерировать код в дев-тайм, у вас не будет потребности его генерировать в ран-тайм. Ран-тайм это такой дев-тайм только вам не надо заниматься контролем изменений сгенерированных файлов (но надо заниматься их кэшированием).

Вот только в run time существует альтернатива генерации шаблонов.

Однако альтернатива генерации шаблонов не оптимальна — иначе бы давно все использовали один шаблон для всего (по крайней мере однотипного). «Кручение по мете» в ран тайм (т.е. «по reflection») это не тоже самое что кодогенерация (один раз покрутились по мете, построили функцию, скомпилировали и переиспользуем). И выигрыш не в скорости (хотя и в ней тоже) а в более строгой систематизации кода, позволяющему метапрограммированию решать все более сложные/общие задачи.

Особо странно что Razor то как раз имеет компиляцию и прикомпеляцию и по сути Razor мог быть легко промежуточным кодом.
более строгой систематизации кода, позволяющему метапрограммированию решать все более сложные/общие задачи

За счет чего?

Мои коллеги, «цикл по метаданным» противопоставляют то «кодогенерации» то «кодогенерации рантайм». Сейчас опять «кодогенерации» в целом. Это путает их самих. Если у вас есть сомнения в том что «кодогенерация» (на самом деле создание функций высших порядков), пусть дев-тайм, позволяет создать более проверяемый и стабильный чем «цикл по метаданным», то я не имею возможности эти сомнения здесь развеять. Считайте это убийственным аргументом. Но если вы принимаете кодогенерацию как рабочий подход — я берусь показать что кодогенерация ран-тайм позволяет идти дальше чем дев-тайм.

Функции высших порядков можно создавать и без кодогенерации…
Только если она не возвращает шаблон Razor'а (в ран-тайм). «Шаблон разора» конечно условно, дуализм фунция-объект. На самом деле Шаблон Разора это такая функция.
П.С. Мы говорил о IViewEngine я помню. Все же это решение без Razorа

Зачем ей возвращать целый шаблон если можно вернуть Func<T, HelperResult>?


И почему вдруг IViewEngine — это без Razor? Кто-то запрещает его использовать или как?

А почему вы считаете что код возвращающи Func<T, HelperResult> будет проще чем код генерирующий шаблон Razor? Разве комбинировать сущности высокой абстракции не будет проще чем низкой (например в смысле количества операций и затрат времени). Допустим вам не нравится операция конкатенции из принципа, но разве все притензии к этой операции будут вас волновать после того как кодогенератор протестирован (кого волнует что T4 это таже конкатенция). Пользователю кодогенератора не надо ничего конкатенировать, у него просто два вызова Create(parameters) и Compile.

Я могу доверять в этом ServiceStack которые утверждают что это (Razor в runtime) это не возможно?

Вы используете в своей практике T4, Expression Trees или roslyn? Я не о том что мол «если не используете не о чем с вами говорить». Просто я до сих пор не понял вашу позицию. «вернуть Func<T, HelperResult>» это аргумент против кодогенерации вообще а не кодогенерации ран тайм.
Я могу доверять в этом ServiceStack которые утверждают что это (Razor в runtime) это не возможно?

Либо вы не так поняли, либо лучше им не доверять...


Просто я до сих пор не понял вашу позицию. «вернуть Func<T, HelperResult>» это аргумент против кодогенерации вообще а не кодогенерации ран тайм.

Мне не нравится кодогенерация там где от нее нет никакой выгоды.

Еще для уточнения вашей позиции:

А когда вы давеча говорили что «в Razor нет eval из коробки» что вы имели ввиду?

Вы используете (вам нравится) возможность генерировать шаблоны Razor девтайм используя T4?
Да что вы докопались до этой кодогенерации Razor через T4? Нет, подобные извращения мне не нравятся.
потому что мне казалось важным выяснить понимаете ли вы что аргументы «крутить по мете», «вернуть Func<T, HelperResult>» они против генерации Razor вообще, dev time или run time — безразницы. кажется, понимаете.

а с мнением что генерировать разор — даже дев тайм — это извращение я не спорю, хватает показать, что оно очевидно спорно — Microsoft сам использует T4 для генерации razorа. Такие T4 входят в каждый дистрибутив Visual Studio.

Вы не обижайтесь, вы предложили бы архитектуру если бы вы смогли определить метаданные описывающие `Transport` (точнее операции над ним) из которых можно «произвести» (лучше всего сгенерировать): контроллер, дтошки, биндинги. Вьюшки — не обязательно. У вас в голове все это наверняка есть, но в коде — незадекларировано. Ставьте перед собой амбициозные цели.
Роман, верно ли я понимаю, что вы о названии статьи? Могли бы вы раскрыть содержание вашего комментария более подробно? С удовольствием выслушаю.
// upd: превентивно изменил название, надеюсь, так будет более уместно.
На Хабре не любят экстремальное, но вот в экстремальной форме вопрос мой к вам звучал бы так: можете ли вы написать кодогенератор контроллера и дтошек? У вас есть класс, есть мета — пусть все остальное будет сгенерировано. В сбалансированной форме вопрос должен был бы звучать наверное так: какое формальное представление имеют метаданные по которым можно было бы сгенерировать контроллер и дтошки, а можно и просто закодить руками в механическом режиме. Я не нашел в Вашей статье размышления о таких метаданных. Извиняюсь за непонятные формулировки. Возможно я вам навязываю свои интересы.

Понял вас. В действительности, многие разработчики задумываются о подобной генерации кода, и я не исключение. Однако, меня всегда останавливало следующее:
Если данный генератор кода будет универсальным в предельно общем смысле, то его настройка будет более сложна, чем механическое написание данного кода.
Это из той же серии (утрированно), что и написание некоторой универсальной системы в принципе.
// Когда-то я с товарищами задумал написать программный комплекс, эмпирически решающий подавляющее большинство типичных задач по ТВиМСу с помощью моделирования на основе входных данных, да так, чтобы любой неподготовленный человек сумел воспользоваться. Однако, на стадии прототипирования стало понятно, что настройка и формирование входных данных на комплекс — задача более трудоемкая, чем «по-быстрому» закодить это на том же Python'e и посмотреть результаты. Это впоследствии послужило большим уроком в аспекте универсальных подходов.

А вот шаблоны, гайдлайны — приветствую, ибо там действительно одно и тоже (и это хорошо!). Соответственно, написать генератор кода по заданным шаблонам — пожалуйста.
Все же создание «универсальной/общей в предельном смысле системы» и генерация ДТОшек с контроллером их туда-сюда преобразующим — вещи принципиально разные.

Возможно ведь и такое рассуждение — решение ценно — если вы решили достаточно общую задачу?
Чтобы генератор кода мог генерировать DTO'шки, ему необходимо знать о заложенных правилах бизнес-логики (иными словами, что ожидается на вход от пользователя). Ведь добрая часть полей генерируется сервисом во время запроса (яркий пример — ID).
Чтобы искомый генератор «знал» о данных правилах, их нужно описать. Описать в формализованном и общем виде. А данная операция, на мой взгляд, едва ли менее трудоемкая, чем сразу написать соответствий DTO вручную (это и будет, в частности, манифест который требуется «на вход» генератору).
Однако, в действительности можно представить, что вы в используемой IDE графически выделяете нужные свойства в модели, нажимаете кнопку генерации и получается соответствующий DTO-класс, тогда неплохо.
С контроллером вообще все сложно (впрочем, CRUD функции можно сгенерировать в некотором общем виде для сущности, и сопутствующие сервисы тоже, тут вы правы).
Уверен, что это в любом случае задача IDE.
Речь идет именно о вашей задаче. Ее решение вполне можно было описать формально — я бы это оценил как достижение. При этом понимаю, что некоторые коллеги только завидев слова «кодогенератор» или «метаданные» пустят очередь из минусомета.
Не вижу очевидных причин не соглашаться с вашим утверждением на тему формального описания. Однако, не для хабра это статья получится (а в данном случае даже плашка Tutorial висит). Да и на тему формализма у меня в целом двоякое чувство, ещё со времен работы с математическим аппаратом.
Как Tutorial не воспринимается, возможно из за заголовка (не один я такой, вон бурчат на «академические идеи»). Раз мясо здесь — создание кастомного Html Helper'а TextBoxFor (смелая идея, мне нравится) — наверное так и стоило назвать.
Роман, были порывы назвать статью в таком ключе. Но, к сожалению, далеко не все поймут, о каком HtmlHelper'e идет речь. Более того, тут достаточно «жестко» предлагается прятать DTO во ViewModel, это неразрывная концепция описанной мной идеи.
Также речь не только об TextBoxFor, речь о любом контроле, который поддерживается стандартной реализацией HtmlHelper.
Однако, после ваших слов, ухожу на очередную итерацию, направленную на более подходящее название. Спасибо!
Употребляемый термин «масштабирование модели» в контексте описания данных нуждается в определении. Model Scaling — кажется тоже не встречался.
Спасибо, уточнил искомое в статье.
НЛО прилетело и опубликовало эту надпись здесь
Не совсем понимаю, зачем вы во ViewModel помещаете DTO объект, который, по факту, не имеет к нему никакого логического отношения. Вы ведь смешиваете логику, которая нужна для создания новых объектов (различные валидации в DTO объекте) и объекты отображения, на которых никакой логики в принципе нет.
Используйте в полной мере доступный функционал — есть ведь прекрасные PartialView, ChildAction(в ASP.NET) и ViewComponent(в ASP.NET Core).
Появится у вас задача на той же страничке, отобразить еще какой-то блок информации, не будете же вы и её добавлять во ViewModel?
Помимо того, что вы разделяете логику и отображение, вы еще и избавитесь от обеих ваших проблем
Как я понимаю, проблема в синхронизации id/name, что если существует два различных класса: viewmodel и commandDto не было бы ошибок binding-а html input элементов, если вдруг через некоторое время переделают viewModel и забудут, что нужно еще перенастроить байдинги для commandDto.
Это вы о чем? Какие могут быть ошибки, когда viewModel и commandDto — это два независимых класса без общих членов?

У них имена свойств должны синхронизироваться. Разве нет?

Зачем?

Есть например у viewmodel свойство 'телефон', хелпер создаст input и сгенерирует имя и ид на основе модели, потом пользователь отправляет post на сервер, мы же должны передаем body запроса с уже сформированными полями, соответственно нам нужно знать что сгенерировано В браузере, чтобы понять как нам маппить входные данные на commandDto

Ну так потому и надо создавать input не на основе модели, а на основе dto. Зачем вообще пустое свойство «телефон» в модели?

Почему пустое, мы там например текущее показываем

Если во ViewModel есть какое-то «текущее» значение — тогда в коде вида будет присваивание одного телефона другому. Их имена синхронизировать все еще не обязательно.

Вы предлагаете в этом случае производить синхронизацию в браузере с помощью js?

Зачем? Вы вообще в курсе как PartialView работают?


@{ 
    Html.RenderPartial("_SomeForm", new CommandDto { 
        Tel = Model.Phone 
    });
}

Где вы тут js вообще увидели?

Я уже потерялся, а какой Ваш вариант. Есть текущий номер телефона, его нужно показать в input textbox и после того как пользователь изменит значение, отправить на сервер. Как вы предлагаете это сделать?

Ага, красиво. Спасибо

Спасибо, красиво сделано

Эх, было время когда мы так же писали. Но прошло время, поменялся подход и из проекта выкинули примерно 60% кода с учетом того, что полезные для пользователя функции добавлялись.
И у нас отлично уживается Razor с Vuejs в SPA.

Подсовываете razor-шаблоны в Vuejs? Каким образом?

Ужас какой, нет конечно. На Vuejs реализованы сложные части UI, которые через обычный DOM+jQuery сделать конечно можно, но получается на порядок сложнее Vuejs компонент.
Vuejs компоненты являются частью других компонент, которые могут иметь свой Razor шаблон, так и рендерится обычними helperами. SPA работает с использованием HistoryJS и получает от сервера HTML, в котором может использоваться VueJS.

Я предпочитаю ViewModel такого вида:
class TransportAddViewModel
{
    // public TransportAddDTO AddDTO { get; set; }

    [Required]
    public int TransportTypeId { get; set; }

    [Required]
    [MaxLength(10)]
    public string Number { get; set; }

    public IEnumerable<TransportTypeDTO> TransportTypes { get; set; }
}

Т.е. сам ViewModel является моделью для отображения данных + нужные словари. Контроллер ожидает ваш TransportAddDTO (с такими же полями). Явный минус такого подхода — дублирование атрибутов. Но их приходится дублировать и в Entity.

Можно с вашей моделью использовать html tags и явно указывать Name:
<input name="Number" asp-for="@Model.AddDTO.Number">
Я не понимаю тех кто минусет такие мнения. Ладно меня с «кодогенерацией» сто раз терпевшей провалы, надо приголубить…

Но как вы там устраиваете биндинги — имеет столь малое практическое значение, что огорчаться увидев другое устройство биндингов — не имеет никакого смысла.

Все свободны даже (о боже!) передавать словари через ViewBag кто бы что не говорил. И парсить HttpRequest в свое удовольствие без всяких биндингов на реквест. Во многих случаях будет просто тупо меньше кода, что уже хорошо.
Спасибо зо поддержку :-)
Мне бы больше хотелось, чтобы минусующие предложили свой вариант. Т.к. ваш вариант мне не очень нравится созданием дополнительного костыля.
На данном этапе встает резонный вопрос: в Razor можно будет передать только одну модель (и слава богу), как же тогда использовать TransportAddDTO для генерации HTML-кода внутри данной страницы?
Очень просто! Достаточно в View Model добавить, в частности, данный DTO

Простой путь не самый верный.
Почитайте docs.microsoft.com/ru-ru/aspnet/core/mvc/views/dependency-injection?view=aspnetcore-2.0

Я бы делал как-то:
@model Transport
@inject DictionaryService dictionary
@{
  var AvailableTransportTypes  = dictionary.GetAvailableTransportTypes();
}
@Html.TextBoxFor(m => m.Number)

Впрочем ещё есть вариант с ViewBag, но менее удобен.

Второй момент — часто DTO это никому не нужный boilerplate. На некоторые атрибуты достаточно повесить атрибут [Ignore], чтобы их сериализатор пропускал. В крайнем случае DTO отнаследовать от сущности. Атрибуты валидации, я бы тоже повесил на доменную сущность.
«ViewBag менее удобен». Такой inject просто обменял присваивания ViewBag на выпадание из общей «трубы» action, и по моему цена не стоит удобства. Я не понимаю как вы в одной транзакции словарь и entity достать будете пытаться (если понадобится). Ну или как например получить параметр Verbose логгинга который включен для parent action'а чтобы и в этом месте вызова dictionary.GetAvailableTransportTypes() сделать Verbose. Точнее — я понимаю, новым кодом. Может и не большим. Выкинули строчку с ViewBag — добавили три — все равно смысл размена не понятен.
Смысл размена — разделение логики контроллера и логики отображения.
Что такое «логика контроллера» и чем отличается от «кода контроллера»?
Разве присваивание словаря ViewBag'у — не происходит в контроллере и является кодом контроллера? Почему вдруг `@inject` в Razore вдруг привел к «разделению логики на контроллера и отображения»? Или Вы как и я за реабилитацию ViewBag (реабилитацию в смысле «как вам удобней»)?
Потому что список типов нужен виду, а не контроллеру. От того что метод GetAvailableTransportTypes вызывается в контроллере — он логикой контроллера не становится. Он остается логикой вида, которую написали в контроллере.
Разве разделение такой логики не произошло на уровне вызовов сервисов в контроллере? Что добавляет к этому разделение еще и на уровне файлов?
Ну и каким образом вызов сервисов в контроллере может разделить вид и контроллер?
this.ViewBag.MyDictionary = dictionaryService.GetDictionary();
return service.GetEntity();


Разве тут не «разделина логика»?
Нет конечно же. Контроллер же вынужден вызывать какой-то левый метод, результат которого ему совсем не интересен.
«не интересен контроллеру» — некорректный термин — это программист определяет «что интересно»- например оба вызова могут быть связаны единой телеметрией, транзакцией, correlation token, единым try catch, единым условие verbose logging'а). Но главное что вызывает отторжение — не возможно согласится с таким определением «логики» по которому следует — что раз есть вызов dictionaryService.GetDictionary следует что «неразделена логика». По моему то что называется «логикой» — оно там внутри сервисов, тут же код биндинга на View. Код биндига можно размазывать и делать «по себе», на здоровье, только не забирайте у нас нашу логику.

поправка:
this.ViewBag.MyDictionary = dictionaryService.GetDictionary();
return View("Details", service.GetEntity());

В модели MVC контроллер — это не сборник биндингов на View, а компонент который обрабатывает (пользовательский) ввод.

Вызов dictionaryService.GetDictionary не относится к обработке ввода.

Вы как программист, конечно же, вольны делать что угодно. Но это будет уже не MVC.
Пока не было `@inject` это было не MVC?
MS в своем коде передает словари через ViewBag — разве это не MVC?

Ну вот я написал так контроллер с разделенной логикой — все на два сервиса. Почему она не разделена? «Контроллер — обрабатывает ввод» — это никак не противоречит. Реквест (не знаю что такое «ввод») пользователя вынуждает поднять словарь для select'а, контроллер и поднял, обработал.
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.