Обеспечение взаимодействия Интернет-магазина с базой данных
Цель практического занятия: На предыдущем занятии Интернет-магазин был переведен на технологию ASP.NET, однако для наполнения контента сайта использовались заранее "вбитые" в код данные. Целью данного занятия является рассмотрение возможности подключения Веб-приложения на технологии ASP.NET к базе данных, а также вопросы манипуляции этими данными.
Файлы к практическому занятию Вы можете скачать здесь.
11.1. Динамическая генерация меню
Как уже отмечалось на прошлом занятии, пункты меню могут генерироваться динамически. Это удобно, например, для того, чтобы предоставить пользователям сайта возможность просматривать продукты, сгруппированные по категориям. Для этого, добавим в раздел меню "Продукты" подразделы, соответствующие записям в таблице ProductCategory, а в них добавим подпункты, соответствующие записям в таблице ProductSubcategory. Для того чтобы это сделать, добавим в проект в папку App_Code файл DataSetMenu.xsd. Это можно сделать, выбрав пункт контекстного меню Add New Item, а в открывшемся диалоговом окне элемент DataSet (рис. 11.1).
В результате в проект будет добавлен новый типизированный источник данных DataSetMenu. Чтобы добавить в него таблицы, необходимо открыть в Microsoft Visual Studio окно Server Explorer и установить соединение с базой данных AdventureWorks (рис. 11.2).
После того, как соединение будет установлено, необходимо выбрать таблицы ProductCategory и ProductSubcategory и перетащить их мышкой в окно дизайнера источника данных (рис. 11.3).
увеличить изображение
Рис. 11.3. Дизайнер источника данных DataSetMenu с добавленными таблицами ProductCategory и ProductSubcategory
После добавления таблиц Visual Studio автоматически создаст ряд классов, среди которых можно выделить следующие:
- ProductCategory и ProductSubcategory: эти два класса наследуют от DataTable и представляют собой отсоединенные контейнеры данных для записей из таблиц БД. При этом каждая запись в этой таблице наследует от класса DataRow, предоставляя доступ к значениям в ячейках не только через итератор, например ProductCategoryRow["Name"], но и свойства, например ProductCategoryRow.Name.
- ProductCategoryTableAdapter и ProductSubcategorTableAdapter: эти классы инкапсулируют класс SqlTableAdapter, позволяя получать данные с сервера и передавать изменения, сделанные на клиенте обратно в БД.
При этом стоит отметить, что между таблицами было установлено отношение, которое соответсвует внешнему ключу базы данных FK_ProductSubcategory_ProductCategory_ProductCategoryID и позволяет легко получать связанные между собой категории и подкатегории продуктов.
Перейдем в файл, содержащий серверный код мастера страниц ( Master.master.cs ), и доопределим метод Page_Load.
Рассмотрим три разных способа расширения меню разрабатываемого Интернет-магазина. В первом варианте воспользуемся только что созданным типизированным DataSetMenu:
#region Первый вариант (типизированный DataSet) DataSetMenu dsm = new DataSetMenu(); ProductCategoryTableAdapter pcta = new ProductCategoryTableAdapter(); pcta.Fill(dsm.ProductCategory); ProductSubcategoryTableAdapter psta = new ProductSubcategoryTableAdapter(); psta.Fill(dsm.ProductSubcategory); for (int i = 0; i < dsm.ProductCategory.Count; i++) { var nm = new MenuItem(dsm.ProductCategory[i].Name) { NavigateUrl = "~/products/default.aspx?category=" + dsm.ProductCategory[i].ProductCategoryID }; MainMenu.Items[1].ChildItems.Add(nm); var productSubCategoryRows = dsm.ProductCategory[i].GetProductSubcategoryRows(); for (int j = 0; j < productSubCategoryRows.Count(); j++) { MainMenu.Items[1].ChildItems[i].ChildItems.Add(new MenuItem(productSubCategoryRows[j].Name) { NavigateUrl = "~/products/default.aspx?category=" + dsm.ProductCategory[i].ProductCategoryID + "&subCategory=" + productSubCategoryRows[j].ProductSubcategoryID }); } } #endregion
Разберем приведенный код. На первом шаге мы создаем экземпляр объекта DataSetMenu. Следующие две строчки создают объект ProductCategoryTableAdapter и вызывают его метод Fill, тем самым скачивая целиком таблицу категорий и помещая записи в одну из таблиц объекта dsm. Далее аналогичные действия выполняются для извлечения данных из таблицы подкатегорий. После этого мы пробегаемся циклом for по таблице dsm.ProductCategory и для каждой записи создаем новый объект MenuItem, свойство текст которого означивается именем текущей категории, а свойство NavigateUrl определяется конкатенацией строк "~/products/default.aspx?category=" и идентификатором текущей категории. Затем созданный пункт меню динамически добавляется в коллекцию подразделов второго пункта меню (пункт "Продукты"). Дальше, при помощи вызова метода GetProductSubcategoryRows() для текущей категории извлекаются все связанные подкатегории. После, в цикле создаются новые разделы меню, которые добавляются в коллекцию дочерних элементов пункта меню, соотвествующего текущей категории.
В качестве альтернативы рассмотрим, как аналогичное меню может быть сделано без использования типизированных источников данных:
#region Второй вариант (не типизированный DataSet) DataSet ds = new DataSet(); SqlDataAdapter sda = new SqlDataAdapter(new SqlCommand("usp_ProductCategoryList", new SqlConnection( "Data Source=localhost;Initial Catalog=AdventureWorks;Integrated Security=True")) {CommandType = CommandType.StoredProcedure}); sda.Fill(ds); DataRelation dr = new DataRelation("ProductCategoryProductSubCategory", ds.Tables[0].Columns["ProductCategoryID"], ds.Tables[1].Columns["ProductCategoryID"]); ds.Relations.Add(dr); for (int i = 0; i < ds.Tables[0].Rows.Count; i++) { var nm = new MenuItem(ds.Tables[0].Rows[i]["Name"].ToString()) { NavigateUrl = "~/products/default.aspx?category=" + ds.Tables[0].Rows[i]["ProductCategoryID"] }; MainMenu.Items[1].ChildItems.Add(nm); var productSubCategoryRows = ds.Tables[0].Rows[i].GetChildRows(dr); for (int j = 0; j < productSubCategoryRows.Length; j++) { MainMenu.Items[1].ChildItems[i].ChildItems.Add( new MenuItem(productSubCategoryRows[j]["Name"].ToString()) { NavigateUrl = "~/products/default.aspx?category=" + ds.Tables[0].Rows[i]["ProductCategoryID"] + "&subCategory=" + productSubCategoryRows[j]["ProductSubcategoryID"] }); } } #endregion
Видно, что этот код идентичен тому, который описан в первом примере. Вместо DataSetMenu используется обычный DataSet , а вместо ProductCategoryTableAdapter и SubproductCategoryTableAdapter используется обычный SqlDataAdapter. В этом примере для извлечения категорий и подкатегорий используется хранимая процедура usp_ProductCategoryList, которая возвращет сразу обе таблицы, но при необходимости можно было бы указать нужный SQL-код для извлечения данных в SqlDataAdapter. Также стоит отметить, что в коллекцию отношений объекта ds добавляется отношение, связывающее обе таблицы, иначе метод ds.Tables[0].Rows[i].GetChildRows(dr) не сможет извлечь подкатегории текущей категории.
Оба приведенных варианта, впрочем, недостаточно хороши. Дело в том, что данный код выполняется относительно долго, так как содержит запрос к базе данных и в то же время будет вызываться каждый раз, когда пользователь будет запрашивать страницу. Чтобы этого избежать, слегка модифицируем код первого вырианта, использовав шаблон проектирования "Одиночка" (Singleton). Для этого в начале класса Master добавим следующий код:
private static object _forLock = new object(); private static DataSetMenu _menuItems; public static DataSetMenu MenuItems { get { if (_menuItems == null) { lock (_forLock) { if (_menuItems == null) { _menuItems = new DataSetMenu(); ProductCategoryTableAdapter pcta = new ProductCategoryTableAdapter(); pcta.Fill(_menuItems.ProductCategory); ProductSubcategoryTableAdapter psta = new ProductSubcategoryTableAdapter(); psta.Fill(_menuItems.ProductSubcategory); } } } return _menuItems; } }
Этот код, реализует весь шаблон проектирования "Одиночка". Создается статическая переменная _menuItems типа DataSetMenu, которая будет заполнена данными лишь один раз, при первом обращении к любой странице нашего сайта. Причем код lock() обеспечит то, что данный объект не будет случайно перезаписан несколько раз, если сразу несколько пользователей одновременно запросят страницу. Так как переменная _menuItems является статической, то она будет сохранять свое значение до тех пор, пока приложение не будет остановлено.
Теперь остается повторить участок кода, создающего разделы меню из первого примера:
#region Третий вариант (быстрый) DataSetMenu dsm = MenuItems; for (int i = 0; i < dsm.ProductCategory.Count; i++) { var nm = new MenuItem(dsm.ProductCategory[i].Name) { NavigateUrl = "~/products/default.aspx?category=" + dsm.ProductCategory[i].ProductCategoryID }; MainMenu.Items[1].ChildItems.Add(nm); var productSubCategoryRows = dsm.ProductCategory[i].GetProductSubcategoryRows(); for (int j = 0; j < productSubCategoryRows.Count(); j++) { MainMenu.Items[1].ChildItems[i].ChildItems.Add(new MenuItem(productSubCategoryRows[j].Name) { NavigateUrl = "~/products/default.aspx?category=" + dsm.ProductCategory[i].ProductCategoryID + "&subCategory=" + productSubCategoryRows[j].ProductSubcategoryID }); } } #endregion
Какой бы вариант не был бы выбран, в результате меню будет иметь вид, представленный на рис. 11.4.