ASP.NET Razor: решение некоторых проблем с архитектурой для модели представления

  • Tutorial

image


Введение


Здравствуйте, коллеги!
Сегодня хочу поделиться с вами своим опытом разработки архитектуры View Model в рамках разработки веб-приложений на платформе ASP.NET, используя шаблонизатор Razor.
Описываемые в данной статье технические реализации подходят для всех актуальных на текущей момент версий ASP. NET (MVC 5, Core, etc). Сама статья предназначена для читателей, которые, по меньшей мере, уже имели опыт работы под данным стеком. Также стоит отметить, что в рамках данной мы не рассматриваем саму пользу View Model и её гипотетическое применение (предполагается, что читатель уже знаком с данными вещами), обсуждаем непосредственно реализацию.


Задача


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


Реализация


Пусть модель выглядит следующим образом (простоты ради в искомой не приведены такие вещи как навигационные свойства и прочее):


class Transport
{
    public int Id { get; set; }

    public int TransportTypeId { get; set; }

    public string Number { get; set; }
}

Разумеется, TransportTypeId — внешний ключ на объект типа TransportType:


class TransportType
{
    public int Id { get; set; }

    public string Name { get; set; }
}

Для связи между frontend и backend будем использовать шаблон Data Transfer Object. Соответственно, DTO для добавления автомобиля будет выглядеть примерно следующим образом:


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

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

* Используются стандартные атрибуты валидации из System.ComponentModel.DataAnnotations.


Настало время понять, что же будет View Model для страницы добавления автомобиля. Некоторые разработчики с радостью бы объявили, что таковой будет являться сам TransportAddDTO, однако, это в корне неверно, так как в данный класс нельзя "запихивать" ничего кроме непосредственно информации для backend, необходимой для добавления нового элемента (по определению). А помимо этого на странице добавления могут потребоваться и другие данные: например, справочник типов транспортных средств (на основе которого и выражается впоследствии TransportTypeId). В связи с этим напрашивается примерно следующая View Model:


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

Где TransportTypeDTO в данном случае будет прямым отображением TransportType (а это далеко не всегда так — как в сторону усечения, так и в сторону расширения):


class TransportTypeDTO
{
    public int Id { get; set; }

    public string Name { get; set; }
}

На данном этапе встает резонный вопрос: в Razor можно будет передать только одну модель (и слава богу), как же тогда использовать TransportAddDTO для генерации HTML-кода внутри данной страницы?
Очень просто! Достаточно в View Model добавить, в частности, данный DTO, примерно так:


class TransportAddViewModel
{
    public TransportAddDTO AddDTO { get; set; }
    public IEnumerable<TransportTypeDTO> TransportTypes { get; set; }
}

Теперь то и начинаются первые проблемы. Попробуем добавить стандартный TextBox для "номера ТС" на страницу в нашем .cshtml файле (пусть это будет TransportAddView.cshtml):


@model TransportAddViewModel
@Html.TextBoxFor(m => m.AddDTO.Number)

Это отрендерится в HTML-код примерно следующего содержания:


<input id="AddDTO_Number" name="AddDTO.Number" />

Представим, что часть контроллера с методом добавления транспорта выглядит так (код в соответствии с MVC 5, для Core он будет чуть-чуть отличаться, но суть такая же):


[Route("add"), HttpPost]
public ActionResult Add(TransportAddDTO transportAddDto)
{
    // Некоторая работа с полученным transportAddDto...
}

Тут мы видим, по меньшей мере, две проблемы:


  1. Id и Name атрибуты имеют префикс AddDTO, и, в последствии, если метод добавления транспорта в контроллере по принципу привязки модели попробует сделать биндинг данных, которые пришли от клиента, в TransportAddDTO, то объект внутри будет состоять полностью из нулей (значений по умолчанию), т.е. это будет просто новый пустой экземпляр. Оно и логично — биндер ожидал имена вида Number, а не AddDTO_Number.
  2. Пропали все мета-атрибуты, т.е. data-val-required и все другие, которые мы так тщательно описывали в AddDTO в виде атрибутов валидации. Для тех кто использует всю мощь Razor это критично, так как это существенная потеря информации для frontend.
    Нам повезло, и они имеют соответственные решения.

Данные вещи "работают" и при использовании, например, враппера для Kendo UI (т.е. @Html.Kendo().TextBoxFor() и др.).


Начнем со второй проблемы: причина тут кроется в том, что в View Model переданный экземпляр TransportAddDTO имел значение null. А реализация механизмов рендеринга такова, что атрибуты при таком случае считываются по меньшей мере не полностью. Решение, соответственно, очевидно — предварительно во View Model инициализировать свойство TransportAddDTO экземпляром класса с помощью конструктора по умолчанию. Лучше это сделать в сервисе, который возвращает инициализированную View Model, однако, в рамках примера подойдет и так:


class TransportAddViewModel
{
    public TransportAddDTO AddDTO { get; set; } = new TransportAddDTO();
    public IEnumerable<TransportTypeDTO> TransportTypes { get; set; }
}

После данных изменений результат будет похож на:


<input data-val="true" id="AddDTO_Number" name="AddDTO.Number" data-val-required="The Number field is required." data-val-length="The field Number must be a string with a maximum length of 10." data-val-length-max="10" />

Уже лучше! Осталось разобраться с первой проблемой — с ней, кстати, всё несколько сложнее.
Для её понимания для начала стоит разобраться что в Razor (подразумевается WebViewPage, экземпляр которого внутри .cshtml доступен как this) представляет собой свойство Html, к которому мы обращаемся с целью вызова TextBoxFor.
Посмотрев на него, можно мгновенно понять, что оно имеет тип HtmlHelper<T>, в нашем случае HtmlHelper<TransportAddViewModel>. Возникает возможное решение проблемы — создать внутри свой HtmlHelper, и передать ему на вход наш TransportAddDTO. Находим минимально возможный конструктор для экземпляра данного класса:


HtmlHelper<T>.HtmlHelper(ViewContext viewContext, IViewDataContainer viewDataContainer);

ViewContext мы можем передать напрямую из нашего экземпляра WebViewPage через this.ViewContext. Разберемся теперь, где взять экземпляр класса, реализующего интерфейс IViewDataContainer. Например, создадим свою реализацию:


public class ViewDataContainer<T> : IViewDataContainer where T : class
{
    public ViewDataDictionary ViewData { get; set; }

    public ViewDataContainer(object model)
    {
        ViewData = new ViewDataDictionary(model);
    }
}

Как можно заметить, теперь мы упираемся в зависимость от некоторого объекта, передаваемого в конструктор с целью инициализации ViewDataDictionary, благо тут всё просто — это и есть экземпляр нашего TransportAddDTO из View Model. То есть получить заветный экземпляр можно так:


var vdc = new ViewDataContainer<TransportAddDTO>(Model.AddDTO);

Соответственно, в создании нового HtmlHelper'a также проблем не возникает:


var Helper = new HtmlHelper<T>(this.ViewContext, vdc);

Теперь можно воспользоваться следующим образом:


@model TransportAddViewModel
@{
var vdc = new ViewDataContainer<TransportAddDTO>(Model.AddDTO);
var Helper = new HtmlHelper<T>(this.ViewContext, vdc);
}
@Helper.TextBoxFor(m => m.Number)

Это отрендерится в HTML-код примерно следующего содержания:


<input data-val="true" id="Number" name="Number" data-val-required="The Number field is required." data-val-length="The field Number must be a string with a maximum length of 10." data-val-length-max="10" />

Как можно заметить, теперь с отрендеренным элементом никаких проблем нет, и он готов к полноценному использованию. Осталось только "причесать" код, дабы он выглядел менее громоздко. Например, расширим наш ViewDataContainer следующим образом:


public class ViewDataContainer<T> : IViewDataContainer where T : class
{
    public ViewDataDictionary ViewData { get; set; }

    public ViewDataContainer(object model)
    {
        ViewData = new ViewDataDictionary(model);
    }

    public HtmlHelper<T> GetHtmlHelper(ViewContext context)
    {
        return new HtmlHelper<T>(context, this);
    }
}

Тогда из Razor можно работать вот так:


@model TransportAddViewModel
@{
var Helper = new ViewDataContainer<TransportAddDTO>(Model.AddDTO).GetHtmlHelper(ViewContext);
}
@Helper.TextBoxFor(m => m.Number)

К тому же, никто не мешает расширить стандартную реализацию WebViewPage таким образом, чтобы она содержала нужное свойство (с сеттером по экземпляру класса DTO).


Заключение


На этом проблемы решены, а также получена архитектура View Model для работы с Razor, которая потенциально может содержать в себе все необходимые элементы.


Стоит отметить, что получившийся ViewDataContainer получился универсальным, и пригоден для использования.


Осталось добавить пару кнопок в наш .cshtml файл, и задача будет выполнена (не учитывая обработки на backend'e). Это я предлагаю сделать самостоятельно.


Если у уважаемого читателя есть идеи как искомое реализовать более оптимальными способами — с радостью выслушаю в комментариях.


С уважением,
Петр Осетров

Поделиться публикацией
Ой, у вас баннер убежал!

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

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

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

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

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

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

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

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

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

                          0

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

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

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

                              За счет чего?

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

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

                                      0

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


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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

                                0
                                Зачем?
                                  0

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

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

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

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

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

                                            0

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


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

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

                                              0

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

                                                0

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

                                                  0

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

                              0

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

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

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

                                0
                                Я предпочитаю 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">
                                  –1
                                  Я не понимаю тех кто минусет такие мнения. Ладно меня с «кодогенерацией» сто раз терпевшей провалы, надо приголубить…

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

                                  Все свободны даже (о боже!) передавать словари через ViewBag кто бы что не говорил. И парсить HttpRequest в свое удовольствие без всяких биндингов на реквест. Во многих случаях будет просто тупо меньше кода, что уже хорошо.
                                    0
                                    Спасибо зо поддержку :-)
                                    Мне бы больше хотелось, чтобы минусующие предложили свой вариант. Т.к. ваш вариант мне не очень нравится созданием дополнительного костыля.
                                  +2
                                  На данном этапе встает резонный вопрос: в 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 отнаследовать от сущности. Атрибуты валидации, я бы тоже повесил на доменную сущность.
                                    0
                                    «ViewBag менее удобен». Такой inject просто обменял присваивания ViewBag на выпадание из общей «трубы» action, и по моему цена не стоит удобства. Я не понимаю как вы в одной транзакции словарь и entity достать будете пытаться (если понадобится). Ну или как например получить параметр Verbose логгинга который включен для parent action'а чтобы и в этом месте вызова dictionary.GetAvailableTransportTypes() сделать Verbose. Точнее — я понимаю, новым кодом. Может и не большим. Выкинули строчку с ViewBag — добавили три — все равно смысл размена не понятен.
                                      0
                                      Смысл размена — разделение логики контроллера и логики отображения.
                                        0
                                        Что такое «логика контроллера» и чем отличается от «кода контроллера»?
                                        Разве присваивание словаря ViewBag'у — не происходит в контроллере и является кодом контроллера? Почему вдруг `@inject` в Razore вдруг привел к «разделению логики на контроллера и отображения»? Или Вы как и я за реабилитацию ViewBag (реабилитацию в смысле «как вам удобней»)?
                                          0
                                          Потому что список типов нужен виду, а не контроллеру. От того что метод GetAvailableTransportTypes вызывается в контроллере — он логикой контроллера не становится. Он остается логикой вида, которую написали в контроллере.
                                            0
                                            Разве разделение такой логики не произошло на уровне вызовов сервисов в контроллере? Что добавляет к этому разделение еще и на уровне файлов?
                                              0
                                              Ну и каким образом вызов сервисов в контроллере может разделить вид и контроллер?
                                                0
                                                this.ViewBag.MyDictionary = dictionaryService.GetDictionary();
                                                return service.GetEntity();


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

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

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

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

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

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

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

                                  Самое читаемое