Цель практического занятия: На предыдущем занятии Интернет-магазин был переведен на технологию ASP.NET, однако для наполнения контента сайта использовались заранее "вбитые" в код данные. Целью
данного занятия является рассмотрение возможности подключения Веб-приложения на технологии ASP.NET к
Файлы к практическому занятию Вы можете скачать здесь.
Как уже отмечалось на прошлом занятии, пункты меню могут генерироваться динамически. Это удобно, например, для того, чтобы предоставить пользователям сайта возможность просматривать продукты, сгруппированные по категориям. Для этого, добавим в раздел меню "Продукты" подразделы, соответствующие записям в таблице ProductCategory, а в них добавим подпункты, соответствующие записям в таблице ProductSubcategory. Для того чтобы это сделать, добавим в проект в папку App_Code файл DataSetMenu.xsd. Это можно сделать, выбрав пункт контекстного меню Add New Item,
а в открывшемся диалоговом окне элемент (рис. 11.1).
(рис 11.1) Окно добавления нового типизированного DataSet в проектВ результате в проект будет добавлен новый типизированный источник данных DataSetMenu. Чтобы добавить в него таблицы, необходимо открыть в AdventureWorks (рис. 11.2).
(рис 11.2) Окно Server Explorer с открытым списком таблиц базы данных AdventureWorksПосле того, как соединение будет установлено, необходимо выбрать таблицы 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, тем самым скачивая целиком таблицу категорий и помещая записи в одну из таблиц объекта . Далее аналогичные действия выполняются для извлечения данных из таблицы подкатегорий. После этого мы пробегаемся циклом for по таблице и для каждой записи создаем новый объект 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 используется обычный , а вместо ProductCategoryTableAdapter и SubproductCategoryTableAdapter используется обычный SqlDataAdapter. В этом примере для извлечения категорий и подкатегорий используется хранимая процедура usp_ProductCategoryList, которая возвращет сразу обе таблицы, но при необходимости можно было бы указать нужный SQL-код для извлечения данных в SqlDataAdapter. Также стоит отметить, что в коллекцию отношений объекта ds добавляется отношение, связывающее обе таблицы, иначе метод ds.Tables[0].Rows[i].GetChildRows(dr) не сможет извлечь подкатегории текущей категории.
Оба приведенных варианта, впрочем, недостаточно хороши. Дело в том, что данный код выполняется относительно долго, так как содержит запрос к 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 является статической, то она будет сохранять свое значение до тех пор, пока приложение не будет остановлено.
_menuItems, иначе пользователи сайта не увидят нового меню. Более того, здесь могут возникнуть проблемы, связанные с тем, что пользователь при первом обращении к странице получит одну структуру меню, а при выборе какого-то пункта, может оказаться что он удален, или указывает на другую страницу. Проблемы правильной организации кэширования данных выходят за рамки данного курса._menuItems на null выполняется несколько раз. Это сделано для того, чтобы еще сэкономить время выполнения кода – операция lock хоть и выполняется достаточно быстро (порядка 40 наносекунд на современных компьютерах) тем не менее требует определенного времени на выполнение. Конечно, в случае с Интренет-магазином это не актуально, но если бы наш сайт занимался обработкой видео-потоков в реальном времени, например поступающих с камер наблюдения, принебрегать этим временем было бы неправильно. Внутри команды lock делается еще одна проверка на null – это сделано для того, чтобы сразу два потока, случайно попавшие во внешний if в момент, когда _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.
(рис 11.4) Вид меню, после добавления категорий и подкатегорийПосле того, как разработано меню, позволяющее ограничивать категорию товара, которой интересуется пользователь, необходимо переработать страницу продуктов так, чтобы она извлекала сведения о продуктах из
Для доступа к данным мы воспользуемся другой технологией, входящей в состав .NET Framework –
Для того, чтобы добавить отображение таблиц данных в виде
(рис 11.5) Добавление классов LINQ to SQL в проектПосле добавление классов в проект, откроется дизайнер классов (рис. 11.6). Добавим в него таблицы ProductCategory, ProductSubcategory, ProductModel, ProductModelIllustrtions, Product, ProductDescriptions, ProductModelProductDescriptionCulture, ProductPhoto, ProductProductPhoto и Productreview, перетащив их мышкой из окна
(рис 11.6) Вид SQL to LINQ классов после добавления таблицПрежде чем приступить к дальнейшей работе, откроем файл DataClasses.designer.cs и увидим сгенерированный код для классов. Перейдем в класс Product и добавим в него следующий код:
public string FullSize
{
get
{
return Size + " " + SizeUnitMeasureCode;
}
}
Product в отдельном файле. Это возможно, так как класс Product объявлен как partial.Теперь мы определили собственное свойство у объекта product, которое отображает его размер с указанными единицами измерений. Стоит отметить, что это свойство вычислимое, оно не хранится в БД и его нельзя изменить.
Теперь перейдем к странице products/default.aspx и переопределим код левого PlaceHolder' а. прежде всего, добавим в него определение нового источника данных – LinqDataSource. Это можно сделать, набрав код руками, а можно перетащить соответствующий элемент из окна Toolbox и настроить его в специальном мастере. Так или иначе, должен получиться следующий код:
<asp:LinqDataSource ID="LinqDataSource1" runat="server" ContextTypeName="DataClassesDataContext" Select="new (ProductID, Name, ProductNumber, Color, ListPrice, Size, SizeUnitMeasureCode, WeightUnitMeasureCode, Weight, FullSize)" TableName="Product"> </asp:LinqDataSource>
В источнике данных LinqDataSource указано, что необходимо брать объекты типа Products, определенные в DataClassesDataContext, и предоставлять доступ компонентам, которые будут использовать этот источник данных только к тем свойствам, которые указаны в атрибуте Select. Теперь переопределим GridView так, чтобы он использовал новый источник данных:
<asp:GridView ID="GridViewProducts" runat="server" CellPadding="4"
GridLines="None" CssClass="GridViewProduct"
AllowSorting="True" AutoGenerateColumns="False"
AllowPaging="True" DataSourceID="LinqDataSource1"
OnSelectedIndexChanged="OnSelectedIndexChanged" >
<RowStyle CssClass="tr_nechet" />
<AlternatingRowStyle CssClass="tr_chet" />
<Columns>
<asp:CommandField SelectText="Выбор" ShowSelectButton="True" />
<asp:BoundField DataField="ProductID" HeaderText="ProductID"
ReadOnly="True"
SortExpression="ProductID" Visible="False" />
<asp:BoundField DataField="ProductNumber" HeaderText="Номер продукта"
ReadOnly="True" SortExpression="ProductNumber" />
<asp:BoundField DataField="Name" HeaderText="Название" ReadOnly="True"
SortExpression="Name" />
<asp:BoundField DataField="Color" HeaderText="Цвет" ReadOnly="True"
SortExpression="Color" />
<asp:BoundField DataField="ListPrice" HeaderText="Цена" ReadOnly="True"
SortExpression="ListPrice" NullDisplayText="Цена не указана"
DataFormatString="{0:F2}" />
<asp:BoundField DataField="FullSize" HeaderText="Размер" ReadOnly="True"
SortExpression="FullSize" />
<asp:TemplateField HeaderText="Вес" SortExpression="Weight" >
<ItemTemplate>
<asp:Label ID="Label1" runat="server"
Text='<%# Eval("Weight") + " " + Eval("WeightUnitMeasureCode") %>'>
</asp:Label>
</ItemTemplate>
</asp:TemplateField>
</Columns>
<PagerStyle BackColor="#FFCC66" ForeColor="#333333" HorizontalAlign="Center" />
<SelectedRowStyle BackColor="#FFCC66" Font-Bold="True" ForeColor="Navy" />
<HeaderStyle CssClass="tr_main" />
</asp:GridView>
Здесь стоит обратить внимание на то, что для GridView установлен источник данных ( DataSourceID="LinqDataSource1" ), разрешены операции сортировки и постраничного вывода ( AllowSorting="True" и AllowPaging="True" ). При этом изменилось и определение колонок – теперь мы можем использовать BoundField, в которых при помощи атрибута указать, какое свойство необходимо отображать. Также, для свойства ListPrice, определяющего цену на товар задан формат вывода ( DataFormatString="{0:F2}" ), который определяет, что строку надо выводить как число с двумя знаками после запятой. Также, для примера, GridView содержит один столбец, использующий TemplateField, который отображает вес товара вместе с единицей измерения веса. Это альтернативный способ добиться отображения сложных полей (для этих же целей мы определили свойство FullSize ).
Теперь необходимо изменить метод Page_Load. В нем нам необходимо ввести ограничения на извлекаемые данные, ограничив категорию и подкатегорию продуктов. Для этого у объектов LinqDataSource есть свойства Where и WhereParameters. При нажатии на определенный пункт меню, происходит переадресация на страницу products/default.aspx, при этом категория и подкатегория передаются через параметры category и . Прежде всего, необходимо сбросить текущие фильтры, после чего, если определена подкатегория (это можно узнать, проверив содержимое Request.QueryString[" ) то достаточно указать свойство Where = "ProductSubcategoryID == @, а в коллекцию WhereParameters добавить новый параметр " со значением, переданным в строке запроса – Request.QueryString[". Если же подкатегория не была указана, но была указана категория, то свойству where необходимо присвоить значение "ProductSubcategory.ProductCategoryID == @category" и задать параметр category в WhereParameters.
ProductCategoryID связанного с продуктом объекта ProductSubcategory, это не значит, что фильтрация будет происходить уже после извлечения данных из БД. В действительности Product и ProductSubcategory ) при помощи оператора SQL JOIN.Ниже приведен полный код метода Page_Load.
protected void Page_Load(object sender, EventArgs e)
{
GridViewProducts.DataKeyNames = new string[]
{
"ProductID"
};
LinqDataSource1.Where = "";
LinqDataSource1.WhereParameters.Clear();
if (!string.IsNullOrEmpty(Request.QueryString["subCategory"]))
{
LinqDataSource1.Where = "ProductSubcategoryID == @subCategory";
LinqDataSource1.WhereParameters.Add("subCategory", DbType.Int32, Request.QueryString["subCategory"]);
}
else
{
if (!string.IsNullOrEmpty(Request.QueryString["category"]))
{
LinqDataSource1.Where = "ProductSubcategory.ProductCategoryID == @category";
LinqDataSource1.WhereParameters.Add("category", DbType.Int32, Request.QueryString["category"]);
}
}
}
На рис. 11.7 представлен вид страницы продуктов при выбранном пункте меню "Mountain Bikes".
(рис 11.7) Страница продуктов, попадающих в подкатегорию "Mountain Bikes" Теперь переопределим обработчик события OnSelectedIndexChanged следующим образом:
protected void OnSelectedIndexChanged(object sender, EventArgs e)
{
if (GridViewProducts.SelectedValue != null)
{
DataClassesDataContext dcdc = new DataClassesDataContext(
"Data Source=localhost;Initial Catalog=AdventureWorks;Integrated Security=True");
var query = dcdc.Products.Single(p => p.ProductID == Convert.ToInt32(GridViewProducts.SelectedValue));
ProductDetails.LoadContent(query);
}
}
Здесь мы применяем технологию DataClassesDataContext, после чего извлекаем из таблицы Products одну запись (метод Single ), причем такую, что ее ProductID равняется значению, сохраненному в выбранной строке. После этого вызовем метод LoadContent объекта ProductDetails.
Далее перепишем компонент ProductDetails. Его полный код можно посмотреть в приложенном примере, здесь мы рассмотрим только ключевые изменения.
Прежде всего, добавим в компонент ProductDetails компонент , который будет отвечать за отображение комментариев пользователей о продукте. Компонент DataView работает с табличными данными, но в отличие от GridView, он отображает данные в виде списка, где для каждого элемента списка задан шаблон.
<asp:DataList ID="ReviewList" runat="server" Width="100%" CellPadding="0" CellSpacing="0">
<ItemTemplate>
<asp:Label ID="Label1" style='font-weight:bold' Text='<%# DataBinder.Eval(Container.DataItem, "ReviewerName") %>'
runat="server" />
<span class="Normal">говорит... </span>
<asp:Image ID="Image1" runat="server" ImageUrl='<%# "~/images/ReviewRating" +
DataBinder.Eval(Container.DataItem, "Rating") +".gif" %>' />
<br>
<asp:Label ID="Label2" CssClass="Normal" Text='<%# DataBinder.Eval(Container.DataItem, "Comments") %>'
runat="server" />
</ItemTemplate>
<SeparatorTemplate>
<br>
</SeparatorTemplate>
</asp:DataList>
Никаких новых атрибутов в теге не определено. В теге ItemTemlate задается отображение каждой записи, код здесь идентичен коду в TemplateField компонента GridView ( DataBinder.Eval(Container.DataItem, "ReviewerName") является эквивалнтом более короткой записи Eval("ReviewerName") ). Тег SeparatorTemplate содержит описание разделителя между записями, в данном случае это пустая строка.
Теперь необходимо переписать метод LoadContent:
public void LoadContent(Product p)
{
Visible = true;
Session["ViewProduct"] = p.ProductID;
description.Text = (p.ProductModel != null
p.ProductModel.ProductModelProductDescriptionCultures.Count > 0)
? p.ProductModel.ProductModelProductDescriptionCultures[0].ProductDescription.
Description
: "";
UnitCost.Text = p.ListPrice.ToString("F2");
ModelName.Text = p.ProductModel != null ? p.ProductModel.Name : "";
ProductNumber.Text = p.ProductNumber.ToString(CultureInfo.InvariantCulture);
ProductImage.ImageUrl = "~/products/images/ProductImages/" +
p.ProductProductPhotos[0].ProductPhoto.LargePhotoFileName;
ReviewList.DataSource = null;
ReviewList.DataSource = p.ProductReviews;
ReviewList.DataBind();
Comment.Text = "";
Email.Text = "";
Rating5.Checked = true;
Name.Text = "";
}
Установим свойство Visible компонента в true, чтобы пользователи его смогли увидеть, после чего поместим идентификатор переданного объекта p в сессию, чтобы с ним можно было позднее работать. Далее обозначим все Label' ы компонента данными из описания товара, а также зададим свойство ImageUrl объекта ProductImage, чтобы отобразить изображение, соответствующее выбранному продукту. Для этого обратимся к коллекции ProductProductPhotos, возьмем первую запись (здесь надо проверить, что такая запись существует), далее по свойству ProductPhoto перейдем к записи в таблице "ProductPhoto" и получим имя файла с изображения из свойства LargePhotoFileName. При этом все изображения должны быть заранее помещены в папку ~/products/images/ProductImages/.
Последнее, что нам необходимо сделать – указать источник данных для ReviewList,и привязать его к этому источнику данных. В данном случае в качестве источника данных выступает список комментариев p.ProductReviews.
На рис. 11.8 показано, как будет выглядеть страница продукта при выбранном в таблице товаре.
(рис 11.8) Страница продуктов, с выбранным в таблице товаромТеперь продемонстрируем на примере комментариев, каким образом можно редактировать существующие и заносить в ProductDetails кодом, который будет содержать поля для создания комментария:
<tr>
<td valign="top" width="100%">
<br>
<asp:Label ID="Label3" runat="server" Text="Label" CssClass="SubContentHead">Добавление нового комментария:</asp:Label>
<br>
<br>
<span class="NormalBold">Имя</span>
<br>
<asp:TextBox MaxLength="50" ID="Name" runat="server" />
<asp:RequiredFieldValidator ControlToValidate="Name" Display="Dynamic" Font-Names="verdana"
Font-Size="9pt" ErrorMessage="Поле 'Имя' должно быть заполнено." runat="server"
ID="RequiredFieldValidator1"></asp:RequiredFieldValidator>
<br>
<br>
<span class="NormalBold">Почтовый адрес</span>
<br>
<asp:TextBox ID="Email" MaxLength="50" runat="server" />
<asp:RequiredFieldValidator ControlToValidate="Email" Display="Dynamic" Font-Names="verdana"
Font-Size="9pt" ErrorMessage="Поле 'Почтовый адрес' должно быть заполнено." runat="server"
ID="RequiredFieldValidator2"></asp:RequiredFieldValidator>
<br>
<br>
<span class="NormalBold">Рейтинг</span>
<br>
<br>
<asp:RadioButton ID="Rating5" Checked="true" GroupName="Rating" Text=" " runat="server" />
<asp:Image ID="ImageR5" runat="server" ImageUrl="~/Images/reviewrating5.gif" />
<br />
<asp:RadioButton ID="Rating4" Checked="false" GroupName="Rating" Text=" " runat="server" />
<asp:Image ID="ImageR4" runat="server" ImageUrl="~/Images/reviewrating4.gif" />
<br />
<asp:RadioButton ID="Rating3" Checked="false" GroupName="Rating" Text=" " runat="server" />
<asp:Image ID="ImageR3" runat="server" ImageUrl="~/Images/reviewrating3.gif" />
<br />
<asp:RadioButton ID="Rating2" Checked="false" GroupName="Rating" Text=" " runat="server" />
<asp:Image ID="ImageR2" runat="server" ImageUrl="~/Images/reviewrating2.gif" />
<br />
<asp:RadioButton ID="Rating1" Checked="false" GroupName="Rating" Text=" " runat="server" />
<asp:Image ID="ImageR1" runat="server" ImageUrl="~/Images/reviewrating1.gif" />
</td>
</tr>
<tr>
<td>
<br>
<span class="NormalBold">Комментарий</span>
<br>
<asp:TextBox ID="Comment" TextMode="multiline" MaxLength="3850" Rows="7" Width="100%"
runat="server" />
<asp:RequiredFieldValidator ControlToValidate="Comment" Display="Dynamic" Font-Names="verdana"
Font-Size="9pt" ErrorMessage="Поле 'Комментарий' должно быть заполнено." runat="server"
ID="RequiredFieldValidator3"></asp:RequiredFieldValidator>
<br>
</td>
</tr>
<tr>
<td>
<br>
<asp:Button ID="AddReview" runat="server" OnClick="OnClick" Text="Добавить свой комментарий" />
<br>
</td>
</tr>
Этот код определяет TextBox, куда пользователь должен ввести свое имя, TextBox для указания адреса электронной почты, набор RadioButton' ов для указания рейтинга продукта, многострочный TextBox' а для комментариев (многострочность указывается атрибутом TextMode="multiline", а количество строк – Rows="7" ) и кнопки, которой будет задан серверный обработчик события OnClick. Здесь также стоит обратить внимание на то, что для каждого текстового поля задан компонент RequiredFieldValidator, который в случае, если текстовое поле пустое, а пользователь нажимает на кнопку (в общем случае – выполняет любое действие, которое приводит к postback' у) предотвращает отправление запроса на сервер и выводит сообщение об ошибке.
Теперь определим обработчик OnClick на сервере:
protected void OnClick(object sender, EventArgs e)
{
DataClassesDataContext dcdc =
new DataClassesDataContext(
"Data Source=localhost;Initial Catalog=AdventureWorks;Integrated Security=True");
var product = dcdc.Products.Single(p => p.ProductID == (int)Session["ViewProduct"]);
ProductReview pr = new ProductReview();
pr.Comments = Comment.Text;
pr.EmailAddress = Email.Text;
if (Rating1.Checked)
{
pr.Rating = 1;
}
if (Rating2.Checked)
{
pr.Rating = 2;
}
if (Rating3.Checked)
{
pr.Rating = 3;
}
if (Rating4.Checked)
{
pr.Rating = 4;
}
if (Rating5.Checked)
{
pr.Rating = 5;
}
pr.ReviewDate = DateTime.Now;
pr.ModifiedDate = DateTime.Now;
pr.ReviewerName = Name.Text;
product.ProductReviews.Add(pr);
dcdc.SubmitChanges();
LoadContent(product);
}
При помощи ProductReview, и означим свойства этого объекта данными, которые пользователь указал в соответствующих компонентах страницы (эти данные были переданы на сервер через ViewState и при восстановлении страницы ASP.NET записал эти значения в свойства соответствующих компонентов). Далее добавим созданный объект pr в коллекцию комментариев продукта – product.ProductReviews, и вызовем метод SubmitChanges контекста, который проанализирует изменения, сгенерирует и выполнит все необходимые запросы к
Аналогичным образом можно реализовать редактирование и добавление любых сущностей. На рис. 11.9 представлен внешний вид карточки добавления комментария.
(рис 11.9) Компонент добавления новых комментариев к продуктуДанное практическое занятие рассматривает следующие темы:
DataSet ;Цель практического занятия: На предыдущем занятии Интернет-магазин был переведен на технологию ASP.NET, однако для наполнения контента сайта использовались заранее "вбитые" в код данные. Целью
данного занятия является рассмотрение возможности подключения Веб-приложения на технологии ASP.NET к
Файлы к практическому занятию Вы можете скачать здесь.
Как уже отмечалось на прошлом занятии, пункты меню могут генерироваться динамически. Это удобно, например, для того, чтобы предоставить пользователям сайта возможность просматривать продукты, сгруппированные по категориям. Для этого, добавим в раздел меню "Продукты" подразделы, соответствующие записям в таблице ProductCategory, а в них добавим подпункты, соответствующие записям в таблице ProductSubcategory. Для того чтобы это сделать, добавим в проект в папку App_Code файл DataSetMenu.xsd. Это можно сделать, выбрав пункт контекстного меню Add New Item,
а в открывшемся диалоговом окне элемент (рис. 11.1).
(рис 11.1) Окно добавления нового типизированного DataSet в проектВ результате в проект будет добавлен новый типизированный источник данных DataSetMenu. Чтобы добавить в него таблицы, необходимо открыть в AdventureWorks (рис. 11.2).
(рис 11.2) Окно Server Explorer с открытым списком таблиц базы данных AdventureWorksПосле того, как соединение будет установлено, необходимо выбрать таблицы 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, тем самым скачивая целиком таблицу категорий и помещая записи в одну из таблиц объекта . Далее аналогичные действия выполняются для извлечения данных из таблицы подкатегорий. После этого мы пробегаемся циклом for по таблице и для каждой записи создаем новый объект 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 используется обычный , а вместо ProductCategoryTableAdapter и SubproductCategoryTableAdapter используется обычный SqlDataAdapter. В этом примере для извлечения категорий и подкатегорий используется хранимая процедура usp_ProductCategoryList, которая возвращет сразу обе таблицы, но при необходимости можно было бы указать нужный SQL-код для извлечения данных в SqlDataAdapter. Также стоит отметить, что в коллекцию отношений объекта ds добавляется отношение, связывающее обе таблицы, иначе метод ds.Tables[0].Rows[i].GetChildRows(dr) не сможет извлечь подкатегории текущей категории.
Оба приведенных варианта, впрочем, недостаточно хороши. Дело в том, что данный код выполняется относительно долго, так как содержит запрос к 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 является статической, то она будет сохранять свое значение до тех пор, пока приложение не будет остановлено.
_menuItems, иначе пользователи сайта не увидят нового меню. Более того, здесь могут возникнуть проблемы, связанные с тем, что пользователь при первом обращении к странице получит одну структуру меню, а при выборе какого-то пункта, может оказаться что он удален, или указывает на другую страницу. Проблемы правильной организации кэширования данных выходят за рамки данного курса._menuItems на null выполняется несколько раз. Это сделано для того, чтобы еще сэкономить время выполнения кода – операция lock хоть и выполняется достаточно быстро (порядка 40 наносекунд на современных компьютерах) тем не менее требует определенного времени на выполнение. Конечно, в случае с Интренет-магазином это не актуально, но если бы наш сайт занимался обработкой видео-потоков в реальном времени, например поступающих с камер наблюдения, принебрегать этим временем было бы неправильно. Внутри команды lock делается еще одна проверка на null – это сделано для того, чтобы сразу два потока, случайно попавшие во внешний if в момент, когда _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.
(рис 11.4) Вид меню, после добавления категорий и подкатегорийПосле того, как разработано меню, позволяющее ограничивать категорию товара, которой интересуется пользователь, необходимо переработать страницу продуктов так, чтобы она извлекала сведения о продуктах из
Для доступа к данным мы воспользуемся другой технологией, входящей в состав .NET Framework –
Для того, чтобы добавить отображение таблиц данных в виде
(рис 11.5) Добавление классов LINQ to SQL в проектПосле добавление классов в проект, откроется дизайнер классов (рис. 11.6). Добавим в него таблицы ProductCategory, ProductSubcategory, ProductModel, ProductModelIllustrtions, Product, ProductDescriptions, ProductModelProductDescriptionCulture, ProductPhoto, ProductProductPhoto и Productreview, перетащив их мышкой из окна
(рис 11.6) Вид SQL to LINQ классов после добавления таблицПрежде чем приступить к дальнейшей работе, откроем файл DataClasses.designer.cs и увидим сгенерированный код для классов. Перейдем в класс Product и добавим в него следующий код:
public string FullSize
{
get
{
return Size + " " + SizeUnitMeasureCode;
}
}
Product в отдельном файле. Это возможно, так как класс Product объявлен как partial.Теперь мы определили собственное свойство у объекта product, которое отображает его размер с указанными единицами измерений. Стоит отметить, что это свойство вычислимое, оно не хранится в БД и его нельзя изменить.
Теперь перейдем к странице products/default.aspx и переопределим код левого PlaceHolder' а. прежде всего, добавим в него определение нового источника данных – LinqDataSource. Это можно сделать, набрав код руками, а можно перетащить соответствующий элемент из окна Toolbox и настроить его в специальном мастере. Так или иначе, должен получиться следующий код:
<asp:LinqDataSource ID="LinqDataSource1" runat="server" ContextTypeName="DataClassesDataContext" Select="new (ProductID, Name, ProductNumber, Color, ListPrice, Size, SizeUnitMeasureCode, WeightUnitMeasureCode, Weight, FullSize)" TableName="Product"> </asp:LinqDataSource>
В источнике данных LinqDataSource указано, что необходимо брать объекты типа Products, определенные в DataClassesDataContext, и предоставлять доступ компонентам, которые будут использовать этот источник данных только к тем свойствам, которые указаны в атрибуте Select. Теперь переопределим GridView так, чтобы он использовал новый источник данных:
<asp:GridView ID="GridViewProducts" runat="server" CellPadding="4"
GridLines="None" CssClass="GridViewProduct"
AllowSorting="True" AutoGenerateColumns="False"
AllowPaging="True" DataSourceID="LinqDataSource1"
OnSelectedIndexChanged="OnSelectedIndexChanged" >
<RowStyle CssClass="tr_nechet" />
<AlternatingRowStyle CssClass="tr_chet" />
<Columns>
<asp:CommandField SelectText="Выбор" ShowSelectButton="True" />
<asp:BoundField DataField="ProductID" HeaderText="ProductID"
ReadOnly="True"
SortExpression="ProductID" Visible="False" />
<asp:BoundField DataField="ProductNumber" HeaderText="Номер продукта"
ReadOnly="True" SortExpression="ProductNumber" />
<asp:BoundField DataField="Name" HeaderText="Название" ReadOnly="True"
SortExpression="Name" />
<asp:BoundField DataField="Color" HeaderText="Цвет" ReadOnly="True"
SortExpression="Color" />
<asp:BoundField DataField="ListPrice" HeaderText="Цена" ReadOnly="True"
SortExpression="ListPrice" NullDisplayText="Цена не указана"
DataFormatString="{0:F2}" />
<asp:BoundField DataField="FullSize" HeaderText="Размер" ReadOnly="True"
SortExpression="FullSize" />
<asp:TemplateField HeaderText="Вес" SortExpression="Weight" >
<ItemTemplate>
<asp:Label ID="Label1" runat="server"
Text='<%# Eval("Weight") + " " + Eval("WeightUnitMeasureCode") %>'>
</asp:Label>
</ItemTemplate>
</asp:TemplateField>
</Columns>
<PagerStyle BackColor="#FFCC66" ForeColor="#333333" HorizontalAlign="Center" />
<SelectedRowStyle BackColor="#FFCC66" Font-Bold="True" ForeColor="Navy" />
<HeaderStyle CssClass="tr_main" />
</asp:GridView>
Здесь стоит обратить внимание на то, что для GridView установлен источник данных ( DataSourceID="LinqDataSource1" ), разрешены операции сортировки и постраничного вывода ( AllowSorting="True" и AllowPaging="True" ). При этом изменилось и определение колонок – теперь мы можем использовать BoundField, в которых при помощи атрибута указать, какое свойство необходимо отображать. Также, для свойства ListPrice, определяющего цену на товар задан формат вывода ( DataFormatString="{0:F2}" ), который определяет, что строку надо выводить как число с двумя знаками после запятой. Также, для примера, GridView содержит один столбец, использующий TemplateField, который отображает вес товара вместе с единицей измерения веса. Это альтернативный способ добиться отображения сложных полей (для этих же целей мы определили свойство FullSize ).
Теперь необходимо изменить метод Page_Load. В нем нам необходимо ввести ограничения на извлекаемые данные, ограничив категорию и подкатегорию продуктов. Для этого у объектов LinqDataSource есть свойства Where и WhereParameters. При нажатии на определенный пункт меню, происходит переадресация на страницу products/default.aspx, при этом категория и подкатегория передаются через параметры category и . Прежде всего, необходимо сбросить текущие фильтры, после чего, если определена подкатегория (это можно узнать, проверив содержимое Request.QueryString[" ) то достаточно указать свойство Where = "ProductSubcategoryID == @, а в коллекцию WhereParameters добавить новый параметр " со значением, переданным в строке запроса – Request.QueryString[". Если же подкатегория не была указана, но была указана категория, то свойству where необходимо присвоить значение "ProductSubcategory.ProductCategoryID == @category" и задать параметр category в WhereParameters.
ProductCategoryID связанного с продуктом объекта ProductSubcategory, это не значит, что фильтрация будет происходить уже после извлечения данных из БД. В действительности Product и ProductSubcategory ) при помощи оператора SQL JOIN.Ниже приведен полный код метода Page_Load.
protected void Page_Load(object sender, EventArgs e)
{
GridViewProducts.DataKeyNames = new string[]
{
"ProductID"
};
LinqDataSource1.Where = "";
LinqDataSource1.WhereParameters.Clear();
if (!string.IsNullOrEmpty(Request.QueryString["subCategory"]))
{
LinqDataSource1.Where = "ProductSubcategoryID == @subCategory";
LinqDataSource1.WhereParameters.Add("subCategory", DbType.Int32, Request.QueryString["subCategory"]);
}
else
{
if (!string.IsNullOrEmpty(Request.QueryString["category"]))
{
LinqDataSource1.Where = "ProductSubcategory.ProductCategoryID == @category";
LinqDataSource1.WhereParameters.Add("category", DbType.Int32, Request.QueryString["category"]);
}
}
}
На рис. 11.7 представлен вид страницы продуктов при выбранном пункте меню "Mountain Bikes".
(рис 11.7) Страница продуктов, попадающих в подкатегорию "Mountain Bikes"Теперь переопределим обработчик события OnSelectedIndexChanged следующим образом:
protected void OnSelectedIndexChanged(object sender, EventArgs e)
{
if (GridViewProducts.SelectedValue != null)
{
DataClassesDataContext dcdc = new DataClassesDataContext(
"Data Source=localhost;Initial Catalog=AdventureWorks;Integrated Security=True");
var query = dcdc.Products.Single(p => p.ProductID == Convert.ToInt32(GridViewProducts.SelectedValue));
ProductDetails.LoadContent(query);
}
}
Здесь мы применяем технологию DataClassesDataContext, после чего извлекаем из таблицы Products одну запись (метод Single ), причем такую, что ее ProductID равняется значению, сохраненному в выбранной строке. После этого вызовем метод LoadContent объекта ProductDetails.
Далее перепишем компонент ProductDetails. Его полный код можно посмотреть в приложенном примере, здесь мы рассмотрим только ключевые изменения.
Прежде всего, добавим в компонент ProductDetails компонент , который будет отвечать за отображение комментариев пользователей о продукте. Компонент DataView работает с табличными данными, но в отличие от GridView, он отображает данные в виде списка, где для каждого элемента списка задан шаблон.
<asp:DataList ID="ReviewList" runat="server" Width="100%" CellPadding="0" CellSpacing="0">
<ItemTemplate>
<asp:Label ID="Label1" style='font-weight:bold' Text='<%# DataBinder.Eval(Container.DataItem, "ReviewerName") %>'
runat="server" />
<span class="Normal">говорит... </span>
<asp:Image ID="Image1" runat="server" ImageUrl='<%# "~/images/ReviewRating" +
DataBinder.Eval(Container.DataItem, "Rating") +".gif" %>' />
<br>
<asp:Label ID="Label2" CssClass="Normal" Text='<%# DataBinder.Eval(Container.DataItem, "Comments") %>'
runat="server" />
</ItemTemplate>
<SeparatorTemplate>
<br>
</SeparatorTemplate>
</asp:DataList>
Никаких новых атрибутов в теге не определено. В теге ItemTemlate задается отображение каждой записи, код здесь идентичен коду в TemplateField компонента GridView ( DataBinder.Eval(Container.DataItem, "ReviewerName") является эквивалнтом более короткой записи Eval("ReviewerName") ). Тег SeparatorTemplate содержит описание разделителя между записями, в данном случае это пустая строка.
Теперь необходимо переписать метод LoadContent:
public void LoadContent(Product p)
{
Visible = true;
Session["ViewProduct"] = p.ProductID;
description.Text = (p.ProductModel != null
p.ProductModel.ProductModelProductDescriptionCultures.Count > 0)
? p.ProductModel.ProductModelProductDescriptionCultures[0].ProductDescription.
Description
: "";
UnitCost.Text = p.ListPrice.ToString("F2");
ModelName.Text = p.ProductModel != null ? p.ProductModel.Name : "";
ProductNumber.Text = p.ProductNumber.ToString(CultureInfo.InvariantCulture);
ProductImage.ImageUrl = "~/products/images/ProductImages/" +
p.ProductProductPhotos[0].ProductPhoto.LargePhotoFileName;
ReviewList.DataSource = null;
ReviewList.DataSource = p.ProductReviews;
ReviewList.DataBind();
Comment.Text = "";
Email.Text = "";
Rating5.Checked = true;
Name.Text = "";
}
Установим свойство Visible компонента в true, чтобы пользователи его смогли увидеть, после чего поместим идентификатор переданного объекта p в сессию, чтобы с ним можно было позднее работать. Далее обозначим все Label' ы компонента данными из описания товара, а также зададим свойство ImageUrl объекта ProductImage, чтобы отобразить изображение, соответствующее выбранному продукту. Для этого обратимся к коллекции ProductProductPhotos, возьмем первую запись (здесь надо проверить, что такая запись существует), далее по свойству ProductPhoto перейдем к записи в таблице "ProductPhoto" и получим имя файла с изображения из свойства LargePhotoFileName. При этом все изображения должны быть заранее помещены в папку ~/products/images/ProductImages/.
Последнее, что нам необходимо сделать – указать источник данных для ReviewList,и привязать его к этому источнику данных. В данном случае в качестве источника данных выступает список комментариев p.ProductReviews.
На рис. 11.8 показано, как будет выглядеть страница продукта при выбранном в таблице товаре.
(рис 11.8) Страница продуктов, с выбранным в таблице товаромТеперь продемонстрируем на примере комментариев, каким образом можно редактировать существующие и заносить в ProductDetails кодом, который будет содержать поля для создания комментария:
<tr>
<td valign="top" width="100%">
<br>
<asp:Label ID="Label3" runat="server" Text="Label" CssClass="SubContentHead">Добавление нового комментария:</asp:Label>
<br>
<br>
<span class="NormalBold">Имя</span>
<br>
<asp:TextBox MaxLength="50" ID="Name" runat="server" />
<asp:RequiredFieldValidator ControlToValidate="Name" Display="Dynamic" Font-Names="verdana"
Font-Size="9pt" ErrorMessage="Поле 'Имя' должно быть заполнено." runat="server"
ID="RequiredFieldValidator1"></asp:RequiredFieldValidator>
<br>
<br>
<span class="NormalBold">Почтовый адрес</span>
<br>
<asp:TextBox ID="Email" MaxLength="50" runat="server" />
<asp:RequiredFieldValidator ControlToValidate="Email" Display="Dynamic" Font-Names="verdana"
Font-Size="9pt" ErrorMessage="Поле 'Почтовый адрес' должно быть заполнено." runat="server"
ID="RequiredFieldValidator2"></asp:RequiredFieldValidator>
<br>
<br>
<span class="NormalBold">Рейтинг</span>
<br>
<br>
<asp:RadioButton ID="Rating5" Checked="true" GroupName="Rating" Text=" " runat="server" />
<asp:Image ID="ImageR5" runat="server" ImageUrl="~/Images/reviewrating5.gif" />
<br />
<asp:RadioButton ID="Rating4" Checked="false" GroupName="Rating" Text=" " runat="server" />
<asp:Image ID="ImageR4" runat="server" ImageUrl="~/Images/reviewrating4.gif" />
<br />
<asp:RadioButton ID="Rating3" Checked="false" GroupName="Rating" Text=" " runat="server" />
<asp:Image ID="ImageR3" runat="server" ImageUrl="~/Images/reviewrating3.gif" />
<br />
<asp:RadioButton ID="Rating2" Checked="false" GroupName="Rating" Text=" " runat="server" />
<asp:Image ID="ImageR2" runat="server" ImageUrl="~/Images/reviewrating2.gif" />
<br />
<asp:RadioButton ID="Rating1" Checked="false" GroupName="Rating" Text=" " runat="server" />
<asp:Image ID="ImageR1" runat="server" ImageUrl="~/Images/reviewrating1.gif" />
</td>
</tr>
<tr>
<td>
<br>
<span class="NormalBold">Комментарий</span>
<br>
<asp:TextBox ID="Comment" TextMode="multiline" MaxLength="3850" Rows="7" Width="100%"
runat="server" />
<asp:RequiredFieldValidator ControlToValidate="Comment" Display="Dynamic" Font-Names="verdana"
Font-Size="9pt" ErrorMessage="Поле 'Комментарий' должно быть заполнено." runat="server"
ID="RequiredFieldValidator3"></asp:RequiredFieldValidator>
<br>
</td>
</tr>
<tr>
<td>
<br>
<asp:Button ID="AddReview" runat="server" OnClick="OnClick" Text="Добавить свой комментарий" />
<br>
</td>
</tr>
Этот код определяет TextBox, куда пользователь должен ввести свое имя, TextBox для указания адреса электронной почты, набор RadioButton' ов для указания рейтинга продукта, многострочный TextBox' а для комментариев (многострочность указывается атрибутом TextMode="multiline", а количество строк – Rows="7" ) и кнопки, которой будет задан серверный обработчик события OnClick. Здесь также стоит обратить внимание на то, что для каждого текстового поля задан компонент RequiredFieldValidator, который в случае, если текстовое поле пустое, а пользователь нажимает на кнопку (в общем случае – выполняет любое действие, которое приводит к postback' у) предотвращает отправление запроса на сервер и выводит сообщение об ошибке.
Теперь определим обработчик OnClick на сервере:
protected void OnClick(object sender, EventArgs e)
{
DataClassesDataContext dcdc =
new DataClassesDataContext(
"Data Source=localhost;Initial Catalog=AdventureWorks;Integrated Security=True");
var product = dcdc.Products.Single(p => p.ProductID == (int)Session["ViewProduct"]);
ProductReview pr = new ProductReview();
pr.Comments = Comment.Text;
pr.EmailAddress = Email.Text;
if (Rating1.Checked)
{
pr.Rating = 1;
}
if (Rating2.Checked)
{
pr.Rating = 2;
}
if (Rating3.Checked)
{
pr.Rating = 3;
}
if (Rating4.Checked)
{
pr.Rating = 4;
}
if (Rating5.Checked)
{
pr.Rating = 5;
}
pr.ReviewDate = DateTime.Now;
pr.ModifiedDate = DateTime.Now;
pr.ReviewerName = Name.Text;
product.ProductReviews.Add(pr);
dcdc.SubmitChanges();
LoadContent(product);
}
При помощи ProductReview, и означим свойства этого объекта данными, которые пользователь указал в соответствующих компонентах страницы (эти данные были переданы на сервер через ViewState и при восстановлении страницы ASP.NET записал эти значения в свойства соответствующих компонентов). Далее добавим созданный объект pr в коллекцию комментариев продукта – product.ProductReviews, и вызовем метод SubmitChanges контекста, который проанализирует изменения, сгенерирует и выполнит все необходимые запросы к
Аналогичным образом можно реализовать редактирование и добавление любых сущностей. На рис. 11.9 представлен внешний вид карточки добавления комментария.
(рис 11.9) Компонент добавления новых комментариев к продуктуДанное практическое занятие рассматривает следующие темы:
DataSet ;Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.