Kartsev Evgeny
Введение
Новая библиотека Dynamic Data предназначена для быстрой генерации фронт-энда для баз данных SQL Server.
Ответ на вопрос - "Зачем мне это нужно?" будет рассмотрен в конце статьи.
Пример, рассматриваемый в статье, может показаться очень наивным и простым - я старался описать все мельчайшие подробности, чтобы охватить как можно бОльшую аудиторию читателей. Основная идея - меньше текста, больше картинок, поэтому разработчики с солидным опытом разработки на платформе .NET смогут понять весь процесс, просто просмотрев рисунки в статье.
Системные требования
- Windows XP SP2/Vista/Server 2003/Server 2008
- Visual Studio 2008 SP1 + .NET Framework 3.5 SP1
- SQL Server 2005/2008
Следует сделать одно важное замечание - необходимо, чтобы был установлен пакет обновлений SP1 для Visual Studio 2008, иначе при выборе вида проекта не появится необходимой опции - создание ASP.NET Dynamic Data Web Site.
База данных
В качестве базы данных для примера, рассматриваемого в статье, будет использована значительно упрощенная база данных, для хранения информации об авторах, книгах и издателях.
Рис 1. Схема тестовой базы данных
Существует список авторов (Authors) и издателей (Publishers). Также существует список книг (Books). Каждая книга может быть написана несколькими авторами, и принадлежать только одному издательству, у книги есть цена, которая не может быть менее 1 у.е. и более 1000 у.е., так же у книги есть дата публикации.
Итак, со схемой определились. Теперь приступим к созданию базы данных и наполнению ее тестовыми данными.
Открываем "SQL Server Management Studio" (Рис.2):
Рис 2. Запуск "SQL Server Management Studio"
Жмем кнопку "New Query" (Рис 3):
Рис 3. Создание нового запроса в SSMS
Копируем t-sql из листинга 1:
USE [master]
GO
IF EXISTS(SELECT 1 FROM sys.databases WHERE name='BookShop')
DROP DATABASE [BookShop]
CREATE DATABASE [BookShop]
GO
USE [BookShop]
GO
CREATE TABLE [Authors]
(
Id int IDENTITY(1,1) NOT NULL,
Name nvarchar(150) NULL
)
GO
CREATE TABLE [Books]
(
Id int IDENTITY(1,1) NOT NULL,
Name nvarchar(250) NOT NULL,
Description nvarchar(500) NULL,
IdPublisher int NOT NULL,
Price float NOT NULL,
PublishedAt datetime NOT NULL
)
GO
CREATE TABLE [BooksToAuthors]
(
IdBook int NOT NULL,
IdAuthor int NOT NULL
)
GO
CREATE TABLE [Publishers]
(
Id int IDENTITY(1,1) NOT NULL,
Name nvarchar(150) NOT NULL
)
GO
ALTER TABLE Authors ADD CONSTRAINT PK_Authors
PRIMARY KEY (Id)
GO
ALTER TABLE Books ADD CONSTRAINT PK_Books
PRIMARY KEY (Id)
GO
ALTER TABLE BooksToAuthors ADD CONSTRAINT PK_BooksToAuthors
PRIMARY KEY (IdBook,IdAuthor)
GO
ALTER TABLE Publishers ADD CONSTRAINT PK_Publishers
PRIMARY KEY (Id)
GO
ALTER TABLE Books ADD CONSTRAINT FK_Books_Publishers
FOREIGN KEY (IdPublisher) REFERENCES Publishers(Id)
GO
ALTER TABLE BooksToAuthors ADD CONSTRAINT FK_BooksToAuthors_Books
FOREIGN KEY (IdBook) REFERENCES Books(Id)
GO
ALTER TABLE BooksToAuthors ADD CONSTRAINT FK_BooksToAuthors_Author
FOREIGN KEY (IdAuthor) REFERENCES Authors(Id)
GO
Листинг 1. Скрипт для создания схемы базы данных
Жмем кнопку "Execute" (Рис. 4):
Рис 4. Запуск запроса
В результате в окне сообщения должны получить "Command(s) completed successfully
".
Следующий шаг - заполнение только что созданной базы данных. Для этого выполняем запрос из листинга 2.
USE [BookShop]
GO
DELETE FROM BooksToAuthors
DELETE FROM Books
DELETE FROM Authors
DELETE FROM Publishers
INSERT INTO Authors (Name) VALUES ('Endrew Troelsen')
INSERT INTO Authors (Name) VALUES ('Matthew MacDonald')
INSERT INTO Authors (Name) VALUES ('Mario Szpuszta')
INSERT INTO Authors (Name) VALUES ('Eugene Kartsev')
INSERT INTO Publishers (Name) VALUES ('Apress')
INSERT INTO Publishers (Name) VALUES ('Williamspublishing')
INSERT INTO Publishers (Name) VALUES ('WROX')
INSERT INTO Books (Name, Description, IdPublisher, Price, PublishedAt)
VALUES ('Pro ASP.NET 3.5 in C#2008', '', (SELECT Id FROM Publishers WHERE Name='Apress'), '350.9900', GETDATE())
INSERT INTO Books (Name, Description, IdPublisher, Price, PublishedAt)
VALUES ('Another Interesting Book about .NET', '', (SELECT Id FROM Publishers WHERE Name='WROX'), '199.10', GETDATE())
INSERT INTO Books (Name, Description, IdPublisher, Price, PublishedAt)
VALUES ('Silverlight 2.0', '', (SELECT Id FROM Publishers WHERE Name='Williamspublishing'), '230.50', GETDATE())
INSERT INTO Books (Name, Description, IdPublisher, Price, PublishedAt)
VALUES ('Mu-mu, the beginning', '', (SELECT Id FROM Publishers WHERE Name='Apress'), '1.99', GETDATE())
INSERT INTO BooksToAuthors (IdBook, IdAuthor)
VALUES ((SELECT Id FROM Books WHERE Name='Pro ASP.NET 3.5 in C#2008'), (SELECT Id FROM Authors WHERE Name='Matthew MacDonald'))
INSERT INTO BooksToAuthors (IdBook, IdAuthor)
VALUES ((SELECT Id FROM Books WHERE Name='Pro ASP.NET 3.5 in C#2008'), (SELECT Id FROM Authors WHERE Name='Mario Szpuszta'))
INSERT INTO BooksToAuthors (IdBook, IdAuthor)
VALUES ((SELECT Id FROM Books WHERE Name='Another Interesting Book about .NET'), (SELECT Id FROM Authors WHERE Name='Endrew Troelsen'))
INSERT INTO BooksToAuthors (IdBook, IdAuthor)
VALUES ((SELECT Id FROM Books WHERE Name='Silverlight 2.0'), (SELECT Id FROM Authors WHERE Name='Matthew MacDonald'))
INSERT INTO BooksToAuthors (IdBook, IdAuthor)
VALUES ((SELECT Id FROM Books WHERE Name='Mu-mu, the beginning'), (SELECT Id FROM Authors WHERE Name='Eugene Kartsev'))
Листинг 2. Скрипт для наполнения БД тестовыми данными
Итак, тестовая база данных готова. Можно приступать к созданию сайта на основе ASP.NET Dynamic Data, но прежде следует сделать несколько замечаний.
Очень важно, чтобы структура базы данных была правильная (под правильностью понимается соответствие 3НФ). Данное требование особо остро ощущается при создании приложений на основании Dynamic Data или приложений, использующих автоматически сгенерированную схему классов LINQ2SQL. Но обо всем по порядку.
Основы ASP.NET Dynamic Data
Чтобы не тратить много времени на теорию, предлагаю начать с создания тестового приложения на базе Dynamic Data.
Запускаем Visual Studio 2008.
Выбираем File->New -> WebSite… (Рис 5):
Рис 5. Создание нового веб-сайта
Выбираем тип проекта "Dynamic Data Web Site" сохраняем его в папке "C:\TestApp\DynamicDataSite", если такой папки не существует, она будет создана (Рис 6):
Рис 6. Создание проекта "Dynamic Data Web Site"
После описанных выше шагов будет создан веб-сайт со структурой как показано на рисунке 7:
Рис 7. Структура проекта "ASP.NET Dynamic Data Web Site"
Следующий шаг - генерация классов LINQ2SQL на основании созданной базы данных.
Добавляем "LINQ to SQL Classes" с именем "BookShop.dbml" (рис 8-9):
Рис 8. Добавление LINQ2SQL классов, шаг 1
Рис 9. Добавление LINQ2SQL классов, шаг 2
Visual Studio предложит сохранить сгенерированные классы в папке "App_Code" (Рис 10). Согласимся с этим - жмем "Yes":
Рис 10. Сохранение файлов с C# кодом в папке "App_Code"
Далее будет открыт дизайнер LINQ2SQL, в который необходимо перенести таблицы из базы данных.
Открываем "Server Explorer" в Visual Studio и добавляем новый коннекшн к тестовой базе данных (Рис 11):
Рис 11. Добавление нового соединения к БД
Вписываем необходимые параметры в окне "Add Connection" и жмем кнопку "Test Connection" (Рис 12):
Рис 12. Форма "Add Connection"
Результат тестирования коннекшна должно быть окно, показанное на рис. 13:
Рис 13. Удачное тестирование соединения с БД
Если вы получили ошибку соединения - необходимо вернуться на шаг назад и проверить правильность ввода данных в окне "Add Connection".
После выполненных шагов переносим таблицы из базы данных "BookShop" на форму дизайнера LINQ2SQL (Рис 14):
Рис 14. Перенос таблицы из "Server Explorer" на форму дизайнера "LINQ2SQL"
В результате генерирования сущностей LINQ2SQL дизайнер должен выглядить приблизительно так-же как и на рис. 15:
Рис 15. Сущности на дизайнере LINQ2SQL
Следующим шагом необходимо раскомментировать строку в файле "Global.asax":
model.RegisterContext(typeof(BookShopDataContext), new ContextConfiguration() { ScaffoldAllTables = true });
Хочу обратить ваше внимание, что необходимо подменить значение "YourDataContextType
" на "BookShopDataContext
", а также поменять значение параметра "ScaffoldAllTables
" с false
на true
.
Параметр "ScaffoldAllTables
" указывает на то, что необходимо взять все таблицы из модели LINQ2SQL и сгенерировать для них формы.
Вот и все, что нужно сделать для того, чтобы сайт работал - следующим шагом жмем кнопку "Save All", после чего жмем "F5" и тестируем работоспособность сайта (Рис 16-17):
Рис 16. Тестирование сгенерированного веб-сайта
Рис 17. Тестирование сгенерированного веб-сайта
Сгенерированный веб-сайт - полностью работоспособное приложение, которое является "Лицом" базы данных и позволяет делать CRUD (Create, Read, Update, Delete) операции со всеми сущностями БД + простую выборку.
Вывод: Используя ASP.NET Dynamic Data можно, потратив 15 минут, создать полностью работоспособное приложение, не написав при этом ни строчки кода.
ASP.NET Dynamic Data deep dive
Модель программирования в Dynamic Data основана на атрибутах, поэтому прежде чем приступать к рассмотрению примера "кастомизации" веб-приложения, предлагаю ознакомиться со списком основных атрибутов в мире Dynamic Data. Исчерпывающая информация об атрибутах можно найти в блоге Maira Wenzel.
Описание атрибутов для классов (Сущностей):
Attribute name |
Description |
TableName |
Имя таблицы, которое будет использоваться в url, в списке таблиц на главной странице, а также в качестве имени в разделе. |
DisplayColumn |
Первый параметр атрибута - имя колонки, которое будет использоваться по умолчанию в DropDownList в других разделах на сайте (где есть Foreign Key на текущее свойство). Второй параметр - имя колонки, по которой должна осуществляться сортировка. |
ScaffoldTable |
True/False - отображает или скрывает таблицу. По умолчанию равен True. |
Описание атрибутов для свойств (Property):
Attribute name |
Description |
Required |
Определяет является ли свойство обязательным. Необходимо для валидации. Если равно True, при создании новой записи поле не может быть пустым. |
StringLength |
Определяет длину строки, которое может быть введено в текстовое поле, сгенерированное для свойства. Нужен для валидации. |
Description |
Определяет текст, который будет появляться при наведении курсора на элемент управления (tooltip) в режиме редактирования. |
DisplayName |
Имя свойства, которое будет отображаться в качестве названия колонки (header) в List Mode, а также в качестве названия редактируемого поля в Edit Mode. |
DefaultValue |
Значение по умолчанию, которое будет использовано во-время создания новой записи. |
RegularExpression |
Определяет регулярное выражение, которое будет использовано для валидации значения введенного в элемент управления в режиме редактирования. |
DataType |
Тип данных в который будет конвертироваться значение введенное в элемент управления. |
DisplayFormat |
Формат, для отображения значения свойства в Display/List/Edit режимах. |
Range |
Определяет минимальное и максимальное значение, которое может быть введено в элемент управления. |
ScaffoldColumn |
Определяет, необходимо ли показывать/скрывать свойство. |
UIHint |
Определяем "кастомный" элемент управления для отображения данных свойства. |
На рис.18 представлена схема взаимодействия сущностей с типами Metadata. В общей схеме есть сущности, которые были сгенерированы из базы данных (LINQ2SQL Entities). Так как сгенерированные сущности помечены как partial - это означает, то можно (и нужно) реализовать вторую часть класса.
В конечном итоге, необходимо сделать 2 шага:
1 - Реализовать класс Metadata, который будет содержать описание для необходимых свойств. Сигнатура свойств в классе Metadata должна быть идентичной сигнатуре свойств в классе, сгенерированном LINQ2SQL.
2 - Реазиловать вторую часть сгенерированного LINQ2SQL класса, помеченного как partial и добавить к нему атрибут MetadataType.
Итак, продолжим рассмотрение примера…
Создаем 2 дополнительные папки с именами "BusinessObjects" и "Metadata" в папке "App_Code", в которых создаем файлы бизнес объектов и Metadata, как показано на рис. 19:
Рис 19. Файлы бизнес объектов и мета-информации.
Для простоты примера опишем только сущность "Book", остальные пометим атрибутом [ScaffoldTable(false)]
для того, чтобы не показывать эти таблицы на сайте.
Author.cs:
using System.ComponentModel.DataAnnotations;
[MetadataType(typeof(AuthorMetadata))]
public partial class Author
{
}
AuthorMetadata.cs:
using System.ComponentModel.DataAnnotations;
[ScaffoldTable(false)]
public class AuthorMetadata
{
}
BooksToAuthor.cs:
using System.ComponentModel.DataAnnotations;
[MetadataType(typeof(BooksToAuthorMetadata))]
public partial class BooksToAuthor
{
}
BooksToAuthorMetadata.cs:
using System.ComponentModel.DataAnnotations;
[ScaffoldTable(false)]
public class BooksToAuthorMetadata
{
}
Publisher.cs:
using System.ComponentModel.DataAnnotations;
[MetadataType(typeof(PublisherMetadata))]
public partial class Publisher
{
}
PublisherMetadata.cs:
using System.ComponentModel.DataAnnotations;
[ScaffoldTable(false)]
public class PublisherMetadata
{
}
Итак, рассмотрим основные классы:
Book.cs:
using System.ComponentModel.DataAnnotations;
[MetadataType(typeof(BookMetadata))]
public partial class Book
{
}
BookMetadata.cs:
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Data.Linq;
using System.Web.DynamicData;
[TableName("Книги")]
[DisplayColumn("Name", "Name")]
public class BookMetadata
{
[Required(ErrorMessage = "Поле 'Имя книги' не может быть пустым")]
[StringLength(250)]
[Description("Официальное имя книги")]
[DisplayName("Имя книги")]
[DefaultValue("<Введите имя книги>")]
[RegularExpression("(?!^<Введите имя книги>$).*", ErrorMessage = "Необходимо внести корректное название книги")]
public string Name { get; set; }
[Description("Краткое описание книги")]
[DisplayName("Описание книги")]
[DataType(DataType.MultilineText)]
public string Description { get; set; }
[Description("Цена книги")]
[DisplayName("Цена")]
[DisplayFormat(DataFormatString = "{0:F2}", ApplyFormatInEditMode = true)]
[Required(ErrorMessage = "Поле 'Цена' не может быть пустым")]
[Range(1, 1000, ErrorMessage = "Значение должно быть в пределах 1-1000")]
public double Price { get; set; }
[Description("Издательский дом")]
[DisplayName("Издатель")]
public Publisher Publisher { get; set; }
[ScaffoldColumn(false)]
public EntitySet<BooksToAuthor> BooksToAuthors { get; set; }
}
Назначение атрибутов рассматривается на рисунке 20:
Рис 21. Маппинг атрибутов
В существующей модели есть одно очень важное ограничение - поле для даты (PublishedAt) это текстовое поле, хоть оно и валидируется, но все же неудобно использовать текстовое поле для ввода даты.
Чтобы решить эту проблему необходимо реализовать "TemplateField" элемент управления и добавить его в модель.
Сделаем это…
Добавляем 2 *.ascx элемента управления в папку "FieldTemplates" с именами "RichDateTime.ascx" и "RichDateTime_Edit.ascx" соответственно. Первый для отображения данных в List Mode, второй для отображения данных в Edit Mode (Рис 21):
Рис 21. Файлы для реализации "TemplateField"
Код в файлах:
RichDateTime.ascx:
<%@ Control Language="C#" AutoEventWireup="true"
CodeFile="RichDateTime.ascx.cs"
Inherits="DynamicData_FieldTemplates_RichDateTime" %>
<asp:Literal ID="dateTime" runat="server" Text="<%# FieldValueString %>" />
RichDateTime.ascx.cs:
using System.Web.DynamicData;
using System.Web.UI;
public partial class DynamicData_FieldTemplates_RichDateTime : FieldTemplateUserControl
{
public override Control DataControl
{
get { return dateTime; }
}
}
RichDateTime_Edit.ascx:
<%@ Control Language="C#" AutoEventWireup="true"
CodeFile="RichDateTime_Edit.ascx.cs"
Inherits="DynamicData_FieldTemplates_RichDateTime_Edit" %>
<asp:Calendar ID="calendar" runat="server" />
RichDateTime_Edit.ascx.cs:
using System.Web.DynamicData;
using System.Web.UI;
public partial class DynamicData_FieldTemplates_RichDateTime_Edit : FieldTemplateUserControl
{
protected override void ExtractValues(System.Collections.Specialized.IOrderedDictionary dictionary)
{
dictionary[Column.Name] = ConvertEditedValue(calendar.SelectedDate.ToString());
}
public override Control DataControl
{
get
{
return calendar;
}
}
}
Далее добавляем новое свойство в класс "BookMetadata.cs" с нужными атрибутами:
[DisplayFormat(DataFormatString = "{0:mm-dd-yyyy}")]
[UIHint("RichDateTime")]
public DateTime PublishedAt { get; set; }
Сохраняем и запускаем сайт. Результат показан на рис. 22:
Рис 22. Результат применения TemplateField к свойству с типом DateTime
Как видим, добавление нового View в модель - невероятно простая задача.
Скачать исходный код примера.
Заключение
Итак, ответим на вопрос в начале статьи: "Зачем мне это нужно?" - ASP.NET Dynamic Data - гибкий фреймворк для очень быстрого построения FrontEnd для базы данных SQL Server. Для всех, кто хочет сохранить свое время - категорически рекомендую ознакомиться с этой технологией поближе, это позволит вам сэкономить несколько дней, а то и недель на разработку.
В ASP.NET Dynamic Data существует еще одно очень мощное и гибкое средство, которое называется "Dynamic Data Filtering". На момент написания статьи данное средство поставляется в виде отдельной сборки, которую можно бесплатно загрузить с сайта codeplex.com.
Как и все средства, Dynamic Data имеет ряд ограничений:
- в качестве БД может быть только SQL Server 2005/2008
- существует проблема при переходе из List в Edit Mode - т.к. на странице List.aspx используется UpdatePanel для AJAX запросов, в случае если существует достаточно большое количесвто данных - скажем, более 20 появится, пэйджер (Pager) для перехода к другим страницам в GridView. Проблема в том, что когда мы переходим в Edit Mode со страници List.aspx, и жмем на странице Edit.aspx линк "Update" или "Cancel" возвращаемся на страницу List.aspx, при этом Pager будет установлен на 1-ю страницу, а не на ту, которая была выбрана до перехода на Edit.aspx. Самый простой способ решения проблемы использование ListDetails.aspx вместо List.aspx -> Edit.aspx, при этом все действия будут осуществляться на одной странице и не будет потеряно значение в поле Pager.