ASP.NET MVC: История одного проекта "Еще немного классов" (часть 4)

Сайтостроение | создано: 07.05.2012 | опубликовано: 07.05.2012 | обновлено: 13.01.2024 | просмотров: 59599 | всего комментариев: 8

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

Содержание

ASP.NET MVC: История одного проекта "Готовимся к старту" (часть 1)
ASP.NET MVC: История одного проекта "Всё ради данных" (часть 2)
ASP.NET MVC: История одного проекта "Шаблоны и внешний вид" (часть 3)
ASP.NET MVC: История одного проекта "Еще немного классов" (часть 4)
ASP.NET MVC: История одного проекта "UI - всё для пользователя" (часть 5)
ASP.NET MVC: История одного проекта "UI - Добавление экспоната" (часть 6)
ASP.NET MVC: История одного проекта "UI - Редактирование экспоната" (часть 7)
ASP.NET MVC: История одного проекта "Обработка ошибок" (часть 8)
ASP.NET MVC: История одного проекта "Фильтрация" (часть 9)
ASP.NET MVC: История одного проекта "Поиск" (часть 10)
ASP.NET MVC: История одного проекта "Облако тегов" (часть 11)
ASP.NET MVC: История одного проекта "Главная страница" (часть 12)

Раз обещал - надо делать

В прошлой статье, я сказал, что покажу как подключать шаблоны с сайта jqueryui.com, но руки так и не дошли. Спасибо уважаемым читателям, которые любезно напомнили мне в комментариях. Итак, для того чтобы подключить шаблон надо просто правильно распаковать скаченный архив и прописать пути в шаблоне сайта (например, в главном). Я не буду расписывать что куда копировать, я просто покажу структуру моего проекта (в части шаблонов и скритов). Так теперь выглядит папка Content:

Обратите внимание на то, что я просто удалил остальные файлы и папки, которые были связаны с шаблонами по умолчанию. А вот так выглядит теперь папка со скриптами:

Обратите, опять же, внимание, на то, что файлы для стандартных шаблонов (jquery-ui-1.8.19.js и jquery-ui-1.8.19.min) тоже удалены, а добавлен один новый из архива.

А главном шаблоне (_LayoutExtended.cshtml) моего проекта я прописал еще две строки. Одна из них подключает файл таблиц каскадных стилей (css) во главе файла (в теге head):

<link href="@Url.Content("/Content/themes/jquery-ui-1.8.19.custom.css")"
  rel="stylesheet" type="text/css" />

А другая строка, (в самом низу шаблона перед закрывающим тегом body) подключает скрипт:

@Content.Scripts("jquery-ui-1.8.18.custom.min.js", Url)

Вот, собственно говоря, и всё что нужно для того, чтобы подключить шаблон. (Еще раз спасибо, Сергей, за напоминание).

Снова про модель данных

В прошлой статье речь зашла о, так называемой "Ленте анекдотов". А сейчас для этой ленты надо будет сделать класс. Более того, я хочу, чтобы музейные экспонаты можно было помечать специальными метками (тегами), которые помогали бы посетителям в поиске экспонатов в музее. Значит потом я смогу сделать, так называемое облако тегов, что тоже поможет посетителям находить быстро нужный им контент. Вот мой новый класс:

/// <summary>
/// Лента для отбора юмора в музей
/// </summary>
public class Lenta {
  [Key]
  [Display(Name = "Идентификатор")]
  public int Id { get; set; }

  [DataType(DataType.MultilineText)]
  [Display(Name="Содержание")]
  public string Content { get; set; }


  [Display(Name="Добавлено")]
  [DataType(DataType.DateTime)]
  public DateTime CreateAt { get; set; }

  [Display(Name="Голосов")]
  public int VoteCount { get; set; }


  [ForeignKey("Hall")]
  [Required(ErrorMessage = "Зал - обязательное поле")]
  [Display(Name = "Зал")]
  public int HallId { get; set; }

  public virtual Hall Hall { get; set; }
}

Перед тем как продолжить, давайте сделаем некоторые преобразования в существующих классах. Не трудно заменить, что у все трех классах, которые уже существуют в проекте, есть одинаковые свойства. Например, свойство Id. Предлагаю вынести его в базовый класс, пусть этот класс носит гордое имя Humor:

public abstract class Humor {
  /// <summary>
  /// Идентификатор
  /// </summary>
  [Key]
  [Display(Name = "Идентификатор")]
  public int Id { get; set; }
}

Пока все три сущности я унаследую от этого класса, при этом удалив у всех свойство Id. Более того, обратите внимание, что и Lenta и Exhibit оба имеют ссылку на Hall. Создам еще один базовый класс HumоrHall, унаследую его от Humor и теперь соответственно сущности должны быть унаследованы от HumorHall. Кстати, чуть не забыл, я создал еще один базовый абстрактный класс CreationInfo, в которой вынес свойство CreatedAt. Теперь надо добавить класс Tag, унаследовав его от класса CreationInfo, и в класс Exhibit добавить свойство коллекцию Tags, чтобы можно было "ходить" по навигационному свойству.

Для полноты картины покажу обновленную схему моделей и связей:

 

Пока этот вариант с наследственностью в моих классах меня устраевает, дальше, что называется... "будем посмотреть".

Классы в базу положите

Давайте теперь запихнем все нужные классы в DbContext. Для этого уже я буду использовать MvcScaffolding, как и обещал когда-то в предыдущих статьях. Итак, для начала надо установить nuget-пакет, который, как вы уже наверное поняли, называется MvcScaffolding. Пишим в консоли менеджера пакетов команду:

PM> Install-Package MvcScaffolding

Результат работы команды:

PM> Install-Package MvcScaffolding
Attempting to resolve dependency 'T4Scaffolding'.
Attempting to resolve dependency 'EntityFramework (? 4.1.10311.0)'.
Successfully installed 'T4Scaffolding 1.0.5'.
Successfully installed 'MvcScaffolding 1.0.6'.
Successfully added 'T4Scaffolding 1.0.5' to Calabonga.Mvc.Humor.
Successfully added 'MvcScaffolding 1.0.6' to Calabonga.Mvc.Humor.

PM>

Далее можно начинать добавлять классы, контроллеры, представления и репозитории для сущностей. Первая будет Exhibit - команда для нее (опять же в консоле) такая:

PM> Scaffold Controller Museum -DbContextType MuseumContext -ModelType Exhibit -Repository

Лог выполнения команды:

PM> Scaffold Controller Museum -DbContextType MuseumContext -ModelType Exhibit -Repository
Scaffolding HumorController...
Added 'Exhibits' to database context 'Calabonga.Mvc.Humor.Engine.MuseumContext'
Added repository 'Models\ExhibitRepository.cs'
Added controller Controllers\MuseumController.cs
Added Create view at 'Views\Museum\Create.cshtml'
Added Edit view at 'Views\Museum\Edit.cshtml'
Added Delete view at 'Views\HMuseumDelete.cshtml'
Added Details view at 'Views\Museum\Details.cshtml'
Added Index view at 'Views\Museum\Index.cshtml'
Added _CreateOrEdit view at 'Views\Museum\_CreateOrEdit.cshtml'


PM>

Этой командой я сказал чтобы Scaffold сделал мне Controller под названием Museum для сущности Exhibit (см. параметр -ModelType). Сущность я сказал подключить к базе данных (DbContext) под названием MuseumContext (см. параметр -DbContextType) с использованием репозитория (см. параметр -Repository). В результате мой MuseumContext получил в распоряжение новую таблицу:

/// <summary>
/// База данных
/// </summary>
public class MuseumContext : DbContext {

  /// <summary>
  /// Таблица залов
  /// </summary>
  public DbSet<Hall> Halls { get; set; }

  /// <summary>
  /// Экспонаты музея
  /// </summary>
  public DbSet<Exhibit> Exhibits { get; set; }
}

В папке Models появился новый файл ExhibitRepository.cs с таким содержанием:

public class ExhibitRepository : IExhibitRepository
{
    MuseumContext context = new MuseumContext();

    public IQueryable<Exhibit> All
    {
        get { return context.Exhibits; }
    }

    public IQueryable<Exhibit> AllIncluding(params Expression<Func<Exhibit, object>>[] includeProperties)
    {
        IQueryable<Exhibit> query = context.Exhibits;
        foreach (var includeProperty in includeProperties) {
            query = query.Include(includeProperty);
        }
        return query;
    }

    public Exhibit Find(int id)
    {
        return context.Exhibits.Find(id);
    }

    public void InsertOrUpdate(Exhibit exhibit)
    {
        if (exhibit.Id == default(int)) {
            // New entity
            context.Exhibits.Add(exhibit);
        } else {
            // Existing entity
            context.Entry(exhibit).State = EntityState.Modified;
        }
    }

    public void Delete(int id)
    {
        var exhibit = context.Exhibits.Find(id);
        context.Exhibits.Remove(exhibit);
    }

    public void Save()
    {
        context.SaveChanges();
    }
}

public interface IExhibitRepository
{
    IQueryable<Exhibit> All { get; }
    IQueryable<Exhibit> AllIncluding(params Expression<Func<Exhibit, object>>[] includeProperties);
    Exhibit Find(int id);
    void InsertOrUpdate(Exhibit exhibit);
    void Delete(int id);
    void Save();
}

Обратите внимание, что MuseumContext не вливается в конструктор через Dependency Injection, а создается как новый экземпляр. Меня такой результат не устраевает, но такой шаблон по умолчанию заложен в MvcScaffolding. К счастью, шаблоны можно менять и переделывать на своё усмотрение. Чтобы получить доступ к шаблону репозитория введу команду в консоле:

PM> Scaffold CustomTemplate Repository Repository

Лог выполнения которой:

PM> Scaffold CustomTemplate Repository Repository
Added custom template 'CodeTemplates\Scaffolders\T4Scaffolding.EFRepository\Repository.cs.t4'
PM>

Что говорит о том, что в моём проекте создана папка CodeTemplates, в которой другая папка с именем скаффолдера по умолчанию для работы с репозиториями, а в этой уже папке появился файл шаблона. Я изменил немного шаблон, определив конструктор, в который через Dependency Injection "опустил" MuseumContext. Так теперь выглядит шаблон для репозитория, вернее, так выглядит измененная часть шаблона:

public class <#= modelName #>Repository : I<#= modelName #>Repository
{
  private <#= contextName #> context;

  public <#= modelName #>Repository(<#= contextName #> context)
  {
    this.context = context;
  }

Осталось только перегенерировать репозиторий на основании нового шаблона:

PM> Scaffold Repository Exhibit -DbContextType MuseumContext

И лог выполнения команды:

PM> Scaffold Repository Exhibit -DbContextType MuseumContext
MuseumContext already has a member called 'Exhibits'. Skipping...
Models\ExhibitRepository.cs already exists! Pass -Force to overwrite. Skipping...

PM>

Опаньки! Последняя строка лога говорит, что файл уже существует. Тогда, надо бы добавить еще один параметр -Force, что обяжет переписать существующий файл:

PM> Scaffold Repository Exhibit -DbContextType MuseumContext -Force
MuseumContext already has a member called 'Exhibits'. Skipping...
Added repository 'Models\ExhibitRepository.cs'

PM> 

Вот так-то лучше. Теперь все репозитории будут генерироваться в дальнейшем с "правильным" конструктором. На самом деле, работа с MvcScaffolding дает очень много возможностей по генерации кода. Кстати, у меня для сущности Exhibit появились не только контроллер Museum и репозиторий, но и все необходимые (по мнению разработчиков MvcScaffolding) представления (Views) в папке Museum:

 

 Теперь надо бы вновь сгенерированный репозиторий "подключить" к контейнеру:

container.RegisterType<IExhibitRepository, ExhibitRepository>();

Теперь контроллер MuseumController сможет получить этот репозиторий в конструктор, а значит, можно запустить и проверить так ли это. Да! Действительно работает:

Созданная на странице таблица и ссылка, говорит о том, что моя база данных готова принимать те самые данные. Хорошо! Или как говорил один киногерой - "Ляпота!"...

Давайте теперь добавим контроллер для класса Lenta, тем более, что на этот раз сгенерированный при этом контроллер, репозитории и представления меня пока устраивают:

PM> scaffold Controller Lenta -DbContextType MuseumContext -Repository
Scaffolding LentaController...
Added 'Lentas' to database context 'Calabonga.Mvc.Humor.Engine.MuseumContext'
Added repository 'Models\LentaRepository.cs'
Added controller Controllers\LentaController.cs
Added Create view at 'Views\Lenta\Create.cshtml'
Added Edit view at 'Views\Lenta\Edit.cshtml'
Added Delete view at 'Views\Lenta\Delete.cshtml'
Added Details view at 'Views\Lenta\Details.cshtml'
Added Index view at 'Views\Lenta\Index.cshtml'
Added _CreateOrEdit view at 'Views\Lenta\_CreateOrEdit.cshtml'
PM>

Не забудем зарегистрировать репозиторий в контейнере:

container.RegisterType<ILentaRepository, LentaRepository>();

Теперь и страница ленты анекдотов тоже показывается с отрисованной таблицей (сгенерированой по умолчанию MvcScaffolding).

Конкретный вид представления от "ленты анедотов" и самого "музея юмора" я пока приводить не буду, по той простой причине, что это всё равно будет переделано многократно. Да и у вас получится всё равно всё тоже самое, потому что код сгенерирован.

Лирическое отступление... Думаю подробнее поговорить о представления придется позже, и в другой части "истории одного проекта", когда я перенесу данные со старой базы в новую. Тогда можно будет видеть, что именно изменилось, но уже на реальных данных.

А нужен ли контроллер?

Сейчас в проекте есть такие контроллеры и репозитории:

Вопрос следующий: нужен ли мне дополнительный контроллер для меток (Tags). Полагаю, что работа с метками будет происходить только в контролере MuseumController при добавлении нового Exhibit. На ленте объекты не будут помечаться метками. Значить мне потребуется только репозиторий для сущности Tag. Создаю этот репозиторий при помощи магии MvcScaffolding ("...вкалывают роботы - счастлив человек..." слова из песни к кинофильму "Приключения электронника"):

PM> scaffold Repository Tag -DbContextType MuseumContext
Added 'Tags' to database context 'Calabonga.Mvc.Humor.Engine.MuseumContext'
Added repository 'Models\TagRepository.cs'
PM>

Объясняю почему я так поступил. Дело в том, что напрямую с метками я вообще не планирую работать. При добавление новой записи в таблицу Exhibit, я строкой через запятую буду подавать и сами метки (теги). При сохранении, я буду обрабатывать метки, проверяя их наличие в базе. Если тег с таким именем есть в базе, ничего не буду делать, если нет - добавлю. И, соответственно наоборот, если какой-то тег при редактировании записи удален, то я буду проверять его присутствие в других записях, если это был последний раз его использования, тег будет удален из базы. Поэтому, контроллер для меток мне не нужен. Подробнее я всё покажу в последующих статьях.

Заключение

В этой статье были подготовлены модели, контроллеры, репозитории и представления, с которыми предстоит работа далее. К началу работы над следующей статьёй, я перенесу данные из старой базы данных в новую. А так же перенесу пользователей из "второй" базы, которая создавалась временно. Получится одна база с пользователями и перенесенными записями из старой базы.

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

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

(это я из Роберта Мартина почерпнул).

Иерархия вида Humor->CreationInfo кажется сомнительной с точки зрения чистоты кода и его логической целостности.

Александр Педько, не могли бы вы указать первоисточник, особенно то место, где "наследование считается не очень хорошим тоном"... Очень хочется быть в курсе последних нововведений в ООП.

Calabonga, можно почитать Code Complete на тему class coupling (если правильно помню, правильный перевод "связанность"). Чем ниже связанность между классами, тем лучше, поскольку слабо связанные классы проще повторно использовать и поддерживать.

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

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

Хороший пример - иерархия наследования контролов в Windows Forms. Наследуюясь от класса Control, благоданя полиморфизму, они становятся полноценным членом объектной модели Windows Forms. Также они наследуют большой кусок функциональности, уже реализованной в Control, необходимой для обеспечения работы объекта в древе контролов.

Еще один пример - класс Stream. Хотя генерализация в этом случае используется вовсю, можно было обойтись без абстрактного класса и вместо этого ввести интрефейс IStream. Или пару IReadableStream и IWriteableStream. Для потоков, поддерживающих асинхронные операции можно было ввести интерфейс IAsyncReadableStream и IAsyncWriteableStream.

Artyom Krivokrisenko, здесь просто очень простой пример базового класса для модели. В рабочем проекте базовый класс модели, помимо унификации типа ключа, еще несет функционал по "очистке" типа модели от прокси-типов EF. Такая очистка нам нужна для унификации пользовательских блокировок сущностей на уровне BAL, так как боъект сущности попадает из разных сессий, каждая из которых работает в собственном контексте данных. 

Добрый день!

Я совсем не имел ввиду "наследование считается не очень хорошим тоном". Но, как правильно упомянул Артем, есть хорошие иерархии и есть не очень хорошие.

Заведение иерархии предполагает полиморфное использование МЕТОДОВ класса-потомка посредством вызова методов класса родителя.

 Подкласс должен допускать использование во всех контекстах, в которых может быть использован базовый класс. Здесь, в общем, добавить нечего, разве что привести немного примеров.

Я могу иметь класс "Дом" со свойством "Номер" и, относледовав от него класс "Автомобиль", получить свойство "Номер" от родителя.

Хорошая ли эта будет иерархия?

Если хотите обратится к первоисточнику, почитайте про "Принцип подстановки Лисков" (Liskov substitution principle).

Надеюсь, написанное мной не выглядит как "критика ради критики".

Саша.

Уважаемый, Александр Петько, ...

"Я могу иметь класс "Дом" со свойством "Номер" и, относледовав от него класс "Автомобиль", получить свойство "Номер" от родителя."

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

Александр Педько, зацеплюсь за выделенное Вами "МЕТОДОВ". В классическом ООП свойств не было вообще, ибо одно из ее главных парадигм - сокрытие данных. Свойства появились в конкретных реализациях как "синтаксический сахар" для упрощения написания примитивных get\set. В общем случае, нас колько я знаю, даже для "пустых" get\set-ов создается неявное поле класса с неявными же обертками в виде методов  доступа к ним.
Ваш пример создания базового класса со свойством "Номер", и наследования от него класса "Дом", и класса "Автомобиль", не совсем корректен, ибо "номер дома" и "номер машины" имеют разное прикладное значение. Другое дело, системная реализация. Вы же не против, что классы в .NET неявно наследуются от Object, и неявно же получают общее системное свойство "идентификатор объекта", недоступный явно программисту, но используемый внутри .NET?

Ustas

Ваш пример создания базового класса со свойством "Номер", и наследования от него класса "Дом", и класса "Автомобиль", не совсем корректен, ибо "номер дома" и "номер машины" имеют разное прикладное значение.

Эту некорректность я и хотел показать.

 

Что касается наследования методов, то я имел ввиду следующее:

Правильным применением наследования является наследование поведения а не данных.

Что касается всеобщего наследования от object, то тут у нас выбора нет, поэтому дискутировать бессмысленно.