вторник, 31 мая 2011 г.

MVC 3 + scaffolding

А теперь обещанное продолжение статьи ASP.NET MVC 3 + Entity Framework 4.1 Code First. Сегодня я расскажу про то, как быстро создать основу (или прототип) для приложения на ASP.NET MVC 3, в том числе, с помощью NuGet-пакета MvcScaffolding. Надо сказать, что принципы MvcScaffolding пришлись мне по душе и даже вдохновили меня на написание своего пакета для scaffolding’а - Model Scaffolding for ASP.NET MVC. О нем я расскажу в следующих статьях.

Поскольку в прошлой статье я уже рассказал про основные понятия и библиотеки (ASP.NET MVC, Entity Framework), сейчас я не буду на них подробно останавливаться. Пример будет использовать scaffolding, который я уже упоминал в предыдущий раз.

Disclaimer. Отмечу, что scaffolding – это, вообще говоря, быстрая генерация чего-либо, с целью последующей доработки, а не способ быстро написать законченное сложное приложение.

NuGet

Пожалуй, единственное, на чем я забыл остановиться в прошлый раз, это NuGet. Сама идея его не нова (apt-get, Ruby Gems и т.п.), однако то, что это теперь доступно и в Visual Studio очень радует. Следуя описанию с официального сайта:

NuGet is a Visual Studio extension that makes it easy to install and update open source libraries and tools in Visual Studio.

В принципе, добавить нечего, однако я все-таки немного добавлю :)

NuGet полезен не только для установки и обновления, но и предоставляет Package Manager Console для Visual Studio 2010, которая позволяет запускать не относящиеся непосредственно к инфраструктуре NuGet скрипты PowerShell. На основе этой возможности, в частности, и реализован пакет MvcScaffolding. Консоль можно открыть в студии с помощью меню View / Other Windows / Package Manager Console.

Также важно отметить, что при создании своего пакета можно указать зависимости, которые загрузятся и установятся автоматически. Вероятно, для кого-то будет также интересна возможность подключения приватного источника пакетов (простенький сервер или общая папка) наравне с nuget.org.

Более подробно о NuGet и Package Manager можно прочитать в документации на официальном сайте. А тем, кто предпочитает формат блога могу порекомендовать статью Scott Hanselmann – Introducing NuPack Package Management for .NET - Another piece of the Web Stack.

Настройка соединения с базой данных

Для того, чтобы не привязываться к установленным (или не установленным) версиям, я решил использовать Microsoft SQL Server Compact Edition – это легковесная СУБД, для которой не требуется настройка и не нужно запускать сервисы.

Чтобы использовать SQL Server CE достаточно загрузить соответствующий пакет (если SQL Server CE еще не подключен к проекту), выполнив в консоли команду:

PM> Install-Package SqlServerCompact

После этого нужно поправить в web.config существующую строку соединения (по умолчанию используется ApplicationServices) и добавить строку соединения с названием, совпадающим с названием контекста (чтобы дополнительно ничего не настраивать):

<add name="MvcDemoContext"
connectionString="Data Source=|DataDirectory|MyContext.SDF"
providerName="System.Data.SqlServerCe.4.0"/>
<add name="ApplicationServices"
connectionString="Data Source=|DataDirectory|MyContext.SDF"
providerName="System.Data.SqlServerCe.4.0" />

Для справки – до релиза Entity Framework 4.1 (где появилась его поддержка в CodeFirst) можно было скачать NuGet-пакет EFCodeFirst.SqlServerCompact, который (вместе несколькими другими пакетами) добавлял такую поддержку к Entity Framework 4.

Настройка генерации базы данных

Допустим, мы планируем в дальнейшем развивать нашу модель, а это обычно и случается с реальными проектами, не так ли? ;) В этом случае нам понадобится пересоздавать базу данных, если изменилась модель (если оставить все по умолчанию, то в этом случае получим exception). Для этого достаточно в “Global.asax.cs” дописать одну строку в методе “Application_Start”:


Database.SetInitializer(new DropCreateDatabaseIfModelChanges
<MvcDemo.Models.MvcDemoContext>());

На всякий случай уточню несколько моментов:

  • Не используйте этот режим в опытной/промышленной эксплуатации, потому что вы потеряете данные, введенные пользователями.
  • Для заполнения базы тестовыми данными можно унаследовать свой класс от DropCreateDatabaseIfModelChanges и переопределить метод Seed. Подробнее про это можно прочитать в блоге команды ADO.NET.
  • Используется пространство имен “System.Data.Entity” (это если вдруг ваша IDE не позволяет автоматически предлагать добавить using).

Постановка задачи

Давайте поставим задачу так:

  1. Пишем прототип, внешний вид нас пока не интересует.
  2. Нам нужно поддерживать список товаров (идентификатор, название).
  3. Товары разбиты на категории (плоский список)
  4. У каждого товара есть производитель.
  5. Дополнительно у товара будет количество на складе (не заморачиваемся на складской учет).

По сути, мы сейчас описали набор моделей для нашего приложения. Сами модели мы создадим чуть позднее. Что касается создания моделей, то по-правильному их нужно создавать в отдельном проекте типа “Class Library”, однако, надеюсь, вы мне простите их создание в том же проекте :)

Обратите внимание: постановка задачи очень простая, в частности, нет связей много-ко-многим, без которых редко обходятся реальные приложения. Поэтому, чтобы не быть обвиненным в подтасовке фактов ;), хочу отметить – на данный момент официального варианта scaffolding’а связей много-ко-многим нет, однако, при желании, можно написать свой Scaffolder (термин из MvcScaffolding, об этом позже). То же вероятно относится и к другим задачам, которые я не охватил своим примером.

Scaffolding: стандартный способ

Для того, чтобы генерировать контроллеры и представления стандартным способом, нам потребуется установить MVC 3 Tools Update. На всякий случай напомню, что это обновление никак не затрагивает библиотеки, а содержит только обновленные инструменты, в том числе для создания контроллеров и представлений.

Создадим новый проект, выбрав из списка “ASP.NET MVC 3 Web Application”. В процессе нам предложат уточнить параметры проекта:

New ASP.NET MVC 3 Project

Заодно хочу обратить внимание на нововведения в MVC 3 Tools Update – новый шаблон “Intranet Application” и поддержку семантической разметки HTML5. Unit-тесты пока оставлю за кадром, чтобы не раздувать размер статьи.

Для примеров я буду использовать как раз Intranet Application – чтобы было меньше файлов, но осталась возможность использовать авторизацию (мало ли…).

Теперь, после создания проекта, студия встречает нас открытым файлом readme.txt (с описанием настройки в разных IIS). Кстати, тому кто это придумал – от меня пять с плюсом :) Правда сейчас нам это не особо интересно.

Далее мы видим, что в привычной структуре проекта отсутствует AccountController и все связанные с ним файлы – это следствие выбора шаблона для Intranet.

Теперь, давайте создадим простые модели, в соответствии с постановкой задачи выше. Кстати, для упрощения их создания я сделал у себя в Resharper шаблон файла SimpleModel для того, чтобы не создавать каждый раз свойства “Id” и “Name”. Это тоже, в некотором роде, scaffolding, однако я не буду сейчас рассказывать про Resharper вообще и его шаблоны в частности, хотя и очень хочется :)

После применения шаблона класс модели выглядит так:

public class Manufacturer
{
public int Id { get; set; } [Required, StringLength(50), RegularExpression
(@"\w+", ErrorMessage =
"The name must contain only letters and digits.")]
public string Name { get; set; }
}

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

...
public int StockQty { get; set; }
public int CategoryId { get; set; }
public virtual Category Category { get; set; }
public int ManufacturerId { get; set; }
public virtual Manufacturer Manufacturer { get; set; }
...

Сейчас уже можно добавлять контроллеры из контекстного меню в Solution Explorer:

Add Controller Menu

В появившемся диалоге выбираем название контроллера, класс модели и контекст (если нет, создаем новый):

Add Controller Dialog

Путем несложного эксперимента было выяснено, что для указания контекста в отдельной папке (например, Models), необходимо указывать эту папку через точку – “Models.MvcDemoContext”.

Теперь, после нажатия на кнопку “Add” мы можем наблюдать небольшое волшебство (по крайней мере для тех, кто ни разу не видел scaffolding в действии) – к нашему проекту добавляются необходимые файлы и, после запуска приложения, мы можем выполнять все операции набора CRUD (Create/Read/Update/Delete) над нашими моделями. Более того, по умолчанию работает ненавязчивая валидация.

Чтобы не забивать блог скриншотами, покажу только список и редактирование товаров (я предварительно ввел несколько категорий и производителей):

Category - Index

Product - Edit

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

MvcScaffolding - основы

Пакет MvcScaffolding, как гласит страница на NuGet.org, разработан Scott Hanselman и Steve Sanderson для быстрой и настраиваемой генерации контроллеров, представлений и некоторых других элементов для ASP.NET MVC. Разумеется, описание не очень объемное, поэтому тем, кто заинтересовался использованием этого пакета предлагаю ознакомиться с серией статей в блоге одного из авторов: http://blog.stevensanderson.com/category/scaffolding/

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

Выполняем в консоли команду:

PM> Install-Package MvcScaffolding

Эта команда установит заодно и T4Scaffolding (пакет, содержащий инфраструктуру для scaffolding вообще).

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

PM> Scaffold Controller Manufacturer -Repository

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

Для повторного использования разметки по созданию и редактированию используется partial view “_CreateOrEdit.cshtml”. Сейчас я не буду сейчас останавливаться на достоинствах и недостатках такого подхода. Скажу только, что, во-первых, никто не мешает потом отредактировать представления, а, во-вторых, можно переделать шаблоны для генерации и даже сам генератор (об этом подробнее расскажу далее).

С помощью параметра “Repository” мы дополнительно сгенерировали интерфейс “IManufacturerRepository” и класс “ManufacturerRepository”. Чтобы не описывать их детально, просто приведу описание интерфейса:

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

Благодаря этому, мы теперь можем довольно просто использовать Interface Injection, скажем, для модульного тестирования.

MvcScaffolding – настройка

Помимо команды “Scaffold Controller” есть несколько других, подробнее они описаны в статье Standard usage: Typical use cases and options.

Как я говорил ранее, MvcScaffolding позволяет переопределять шаблоны и генераторы, а также создавать новые. Более детально об этом можно прочитать в блоге автора, а здесь я просто покажу один из практических вариантов применения.

Если вы сгенерируете представления с помощью MvcScaffolding, то вы обнаружите, что ненавязчивая валидация для них не работает. Причина в том, что, в отличие от стандартного scaffolding (в MVC 3 Tools Update), в представлениях отсутствуют следующие строки подключения скриптов:

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")"
type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")"
type="text/javascript"></script>

Как вариант, можно их добавить в “_Layout.cshtml” (в чем, опять же, есть свои плюсы и минусы), однако давайте воспроизведем поведение стандартного scaffolding.

Для этого запустим в консоли команду:

PM> Scaffold CustomTemplate MvcScaffolding.RazorView Create

После выполнения этой команды в нашем проекте появится файл “Create.cs.t4”, который является шаблоном T4 и будет теперь использоваться для генерации (при этом вернуться к использованию стандартного можно просто удалив этот шаблон или переименовав его):

Custom template

Теперь просто добавляем необходимую пару строк в этот файл, сохраняем его, и запускаем команду:

PM> Scaffold Controller Manufacturer –Repository –Force

Последний параметр говорит о том, что мы ходим перезаписать наши существующие файлы.

Если мы перезапустим приложение, то увидим, что ненавязчивая валидация заработала.

Резюме

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

Если у вас есть замечания, пожелания или новые темы – пишите в комментариях или на olegaxenow.reformal.ru. Постараюсь учесть.

4 комментария:

  1. Спасибо, Олег, за статью! Хороший обзор получился.
    Вопросы:
    1. Статья про Resharper будет? :)
    2. partial view “_CreateOrEdit.cshtml”. Можно ли где-то почитать про достоинства и недостатки такого подхода? Или Ваши мысли?
    P.S. Жду с нетерпением статью про "Model Scaffolding for ASP.NET MVC", особенно про будущие новшества:)

    ОтветитьУдалить
  2. Всегда пожалуйста :) Отвечаю.

    1. Будет, вероятно не одна. Точной даты пока назвать не могу.
    2. Если вкратце - хорошо тем, что не дублируем, плохо тем, что иногда нужно делать создание и редактирование немного по-разному, а такой подход немного останавливает от того, чтобы взять и разделить на два view (ведь уже работает :)

    ОтветитьУдалить
  3. Пример с модификацией шаблона не удачный. Все это есть по умолчанию. Надо только добавить ключ -ReferenceScriptLibraries.

    ОтветитьУдалить
  4. To @Andrey
    Скажем так, не самый удачный, возможности-то он демонстрирует. Просто параметр ReferenceScriptLibraries как-то не заметил.

    ОтветитьУдалить