Pull to refresh

Борьба с наследованием и вложенностью ViewModel-ей при разработке lolstore.info

Reading time4 min
Views1.2K
Приветствую, хабрачитатель.
Хочу поделиться велосипедом реализацией, к которой пришел в процессе изучения Asp.Net Mvc и разработки бугагашеньки lolstore.info. Мне оная удобна, не исключено что такой станет и для Вас.

Для начала сформулируем проблему/цель:

Сделать всё зашибись. Найти лаконичный и прозрачный способ передачи нескольких(!) типизированных ViewModel-ей из контроллера во View и их рендер с проверкой типов на этапе компиляции.

Ситуация становиться особенно острой, когда для 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;
    } 
Другие extension-ы добавлены исключительно для удобства (тут только пара из них):
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
Tags:
Hubs:
Total votes 18: ↑10 and ↓8+2
Comments7

Articles