Приветствую, хабрачитатель.
Хочу поделитьсявелосипедом реализацией, к которой пришел в процессе изучения Asp.Net Mvc и разработки бугагашеньки lolstore.info. Мне оная удобна, не исключено что такой станет и для Вас.
Сделать всё зашибись. Найти лаконичный и прозрачный способ передачи нескольких(!) типизированных ViewModel-ей из контроллера во View и их рендер с проверкой типов на этапе компиляции.
Ситуация становиться особенно острой, когда для masterpage-а нужна одна ViewModel (html title, доступное меню и т.п), странице — другая (к примеру, список анекдотов), а для pagelet-ов вообще третья (список тегов). Плюс чешется сохранить легкую переносимость тех же pagelet-ов на другие страницы.
Что есть каждая ViewModel каждая в отдельности? — подумал я, потягивая чай. Ответ созрел сам собой: просто контейнер для типизированных данных по возможности без логики (мне тоже нравится логику рендера делать в extension-методах HtmlHelper-ов).
Как передать несколько model-ей существующими средствами из controller-а во View? — еще глоток напитка, — мм, так есть же ViewData, хотя сначала от неё отказался.
Как сделать проверку типов на этапе компиляции и избавиться от поиска по строковом ключу? — Generic спешит на помощь! — закусил печенькой.
Как добавить свою реализацию, но чтобы не пришлось наследоваться от чего-либо для использования? — Таки extensions.
По сути всё свелось к двум методам:
В качестве плюсов можно выделить:
В контроллере:
На View-ах без strong-type наследования:
В HtmlHelper-ах:
Целый весь один файл с кодом можно скачать здесь.
З.Ы. Если подобные эксперименты интересны хабравчанам и могут быть применены не только в моих проектах — то дальше выложу реализацию конкуретного и асинхронного кеширования, простенький AutoMapper (используется для клонирования одного типа объектов в другой без наследований, интерфейсов, но с проверкой на этапе компиляции) и другие полезности.
By EugeneOstapchuk
Хочу поделиться
Для начала сформулируем проблему/цель:
Ситуация становиться особенно острой, когда для masterpage-а нужна одна ViewModel (html title, доступное меню и т.п), странице — другая (к примеру, список анекдотов), а для pagelet-ов вообще третья (список тегов). Плюс чешется сохранить легкую переносимость тех же pagelet-ов на другие страницы.
Ну что ж, начнём разбираться:
- Strong-typed views: вариант достойный для малозависимых страниц. Однако, как было сказано выше, если использовать несколько вложенных masterpage-ов и у каждой своя модель — может начаться жуть с наследованием, вложенностью, диаграмма классов и зависимостей становится похожей на логотип хабра. ИМХО такая запутанность — фе-е… Можно пробывать разделять их интерфейсами. Конечно не исключено, что придумали адекватные архитектурные решения, жаль, но я не встречал или же ими не делятся;
- ViewData[string key]: универсально, доступно из любой View, задействованной в рендере, но поиск по строке-ключу плюс отсутствие типизации делает этот способ добовольно грустным и потенциально глючным. На безрыбье однако тоже можно;
- Свой вариант: Так как найденные способы не устроили, то я пошел по этому пути. И не жалею.
Полет мысли:
Что есть каждая ViewModel каждая в отдельности? — подумал я, потягивая чай. Ответ созрел сам собой: просто контейнер для типизированных данных по возможности без логики (мне тоже нравится логику рендера делать в extension-методах HtmlHelper-ов).
Как передать несколько model-ей существующими средствами из controller-а во View? — еще глоток напитка, — мм, так есть же ViewData, хотя сначала от неё отказался.
Как сделать проверку типов на этапе компиляции и избавиться от поиска по строковом ключу? — Generic спешит на помощь! — закусил печенькой.
Как добавить свою реализацию, но чтобы не пришлось наследоваться от чего-либо для использования? — Таки extensions.
...
Profit:
По сути всё свелось к двум методам:
- Генерация ключа для ViewData:
public static String GetKey<TModel>()
{
return typeof (TModel).FullName;
}
- Поиск модели или её создание со значениями по умолчанию:
public static TModel Model<TModel>(this ViewDataDictionary viewData)
{
String key = GetKey<TModel>();
if (viewData.ContainsKey(key))
{
Object model = viewData[key];
if ((model != null) && (model.GetType().Equals(typeof(TModel))))
{
return (TModel)model;
}
}
TModel newModel = Activator.CreateInstance<TModel>();
viewData[key] = newModel;
return newModel;
}
public static TModel Model<TModel>(this Controller controller)
{
return controller.ViewData.Model<TModel>();
}
public static TModel Model<TModel>(this HtmlHelper htmlHelper)
{
return htmlHelper.ViewData.Model<TModel>();
}
В качестве плюсов можно выделить:
- Чтобы отрисовать несколько независимых моделей не нужно их объединять между собой в какой-либо контейнер. Наверное самое полезное;
- Каждая небольшая модель знает только о себе, как и страница, которая её рендерит. Удобно переносить, оставив заполнение данными controller-у (как и должно быть). Слабосвязанность конкретного action-а со страницей. В результате любую модель можно отрисовать на любой странице;
- Объект модели создается при первом обращении (из controller-а или из view), нет необходимости проверять на null, заботиться о том, чтобы она обязательно была во ViewData. Если где-то забудете её заполнить — она будет со значениями по умолчанию;
- Отпадает необходимость делать StrongTyped вьюхи (ну почти отпадает ;).
- Модель должна иметь public конструктор дабы Activator мог её создать. (Сомнительный такой себе минус. В реальности сделан кеширующий конструкторы Activator с использованием Expressions, который раза в 4 быстрее оригинального);
- Синтаксис немного усложнился.
Использование:
В контроллере:
public ActionResult Default()
{
…
// отрисовывается в pagelet-е
this.Model<TagListViewModel>().MyBlaBlaBlaProperty = "lolstore.info";
// используется на текущей для action-а master-странице
this.Model<UserMenuViewModel>().MyBlaBlaBlaProperty2 = "Анекдоты";
}
На View-ах без strong-type наследования:
<% = this.Model<TagListViewModel>().MyBlaBlaBlaProperty%>
В HtmlHelper-ах:
public static MvcHtmlString RenderSomething(this HtmlHelper htmlHelper)
{
if (!htmlHelper.ModelExists<TagListViewModel>())
{
return new MvcHtmlString(String.Empty);
}
TagListViewModel model = htmlHelper.Model<TagListViewModel>();
// рендер чего нить
}
Целый весь один файл с кодом можно скачать здесь.
З.Ы. Если подобные эксперименты интересны хабравчанам и могут быть применены не только в моих проектах — то дальше выложу реализацию конкуретного и асинхронного кеширования, простенький AutoMapper (используется для клонирования одного типа объектов в другой без наследований, интерфейсов, но с проверкой на этапе компиляции) и другие полезности.
By EugeneOstapchuk