Новые возможности связывания данных в ASP.NET
Вспомним прошлые дни и банальную задачу вывода данных из таблицы базы
данных на asp страницу в виде таблицы. Для этого необходимо было
открывать рекордсет и затем проходя по нему выводить данные с
одновременным форматированием. Например вот так:
<%
set conn = Server.CreateObject("ADODB.Connection")
conn.Open "Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa;
Initial Catalog=Northwind;Data Source=(local);"
set rst = conn.Execute("SELECT ProductName, UnitPrice,
UnitsInStock, Discontinued, CategoryID FROM Products" +
" order by ProductName")
%>
<table ID="Table1">
<tr>
<td>Product</td><td>Price</td><td>
In Stock</td><td>Discontinued</td>
</tr>
<%
while not rst.eof
%>
<tr>
<td><%=
rst("ProductName")%></td><td><%=rst("UnitPrice")%>
</td><td><%=rst("UnitsInStock")%></td>
<td><%=rst("Discontinued")%></td>
</tr>
<%
rst.MoveNext
wend
%>
В мире ASP.NET, как вы наверное уже знаете, такие извращения ни к
чему. Все, что нужно для выполнения данной задачи, это заполнить DataSet
результатами запроса и связать его с DataGrid и DataList. Ничего
сложного, ведь так? Сложности начинаются потом :).
Использования события ItemDataBound для управления отображением
данных
Одна из таких сложностей – форматирование выводимых данных в
зависимости от их значений. Например ваш начальник любит красивые
таблички с цифрами, разрисованные разным цветом в зависимости от того,
что там написано. И в приведенном выше примере начальник требует чтобы
продукты первых четырех категорий имели различный цвет фона. Раньше
решение данной задачи было тривиальным - в цикле вывода данные
предварительно анализировались и в зависимости от различных параметров
форматировались по разному (строились монструозные теги <font> или
устанавливались соответствующие стили. На первый взгляд ничего такого в
ASP.NET сделать нельзя, ведь объект DataGrid - единое целое и выводить с
помощью него данные в цикле не получится. Но не все так плохо - на самом
деле все можно сделать :)
Объект DataGrid (как впрочем и объекты DataList и Repeater) имеет
событие ItemDataBound. Данное событие срабатывает для каждой строки из
источника данных после того, как данные связаны в DataGrid, но до того,
как эти данные выведены. В обработчике данного события вы и можете как
раз повлиять на то, как эти данные будут представлены пользователю.
Второй параметр обрабатчика этого события представляет собой объект типа
DataGridItemEventArgs, с помощью которого можно произвести множество
различных манипуляций с выводимыми данными.
В данной конкретной задаче нас интересуют следующие вопросы:
- Значения данных в текущей строке (нам же нужно форматировать
выводимую информацию на основании каких-то цифр :).
- Доступ к выводимым результатам (действительно - их то и нужно же
отформатировать). Все это (и много другое) можно найти в свойствах
объекта DataGridItemEventArgs.
Поищем для начала исходные данные. Ссылка на объект с ними может быть
найдена в свойстве DataGridItemEventArgs.Item.DataItem. В типизированном
мире .NET это свойство имеет тип object, соответственно его нужно
привести к какому-нибудь более приемлемому типу. Если ваши данные,
которые вы связываете с DataGrid, находятся в объекте DataView, то
данный объект можно смело приводить к DataRowView - не ошибетесь :).
Итак теперь можно писать следующую конструкцию для получения категрии
продукта:
switch((int)((DataRowView) e.Item.DataItem)["CategoryID"])
е в данном случае - объект типа DataGridItemEventArgs.
Теперь неплохо было бы соответствующим образом отформатировать
выводимые данные, т.е. раскрасить фон в зависимости от значения
CatogoryID. Доступ к такой вохможности также нужно искать в свойствах
DataGridItemEventArgs.Item. И найти его несложно - свойство
DataGridItemEventArgs.Item.BackColor позволяет манипулировать цветом
фона выводимой строки. Как раз то, что нам нужно :).
case 1:
e.Item.BackColor = Color.Aqua;
break;
И, наконец, затронем еще один вопрос. Событие ItemDataBound
срабатывает для каждой выводимой строки, не только для строки с данными.
А что будет, если мы попробуем получить объект с данными для строки
заголовков или разделителя? А исключение будет :). Боссу такого
показывать нельзя, ибо босс, любящий исключения, сродни Windows 98,
работающей без ошибок :). Значит перед всеми нашими манипуляциями
необходимо сначала проверить какую строку сейчас мы собираемся
обрабатывать. Опять таки в данной ситуации помощет свойство
DataGridItemEventArgs.Item.ItemType типа ListItemType. Наш код должен
выполняться только для типов Item и AlternatingItem.
if(e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
Вот и все. Полный код asp.net страницы, рисующей разноцветные строки,
представляю:
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Import Namespace="System.Drawing" %>
<HTML>
<script language="C#" runat="server">
void Page_Load(object sender, System.EventArgs e)
{
SqlConnection myConn =
new SqlConnection("server=localhost;database=Northwind;uid=sa;pwd=");
SqlDataAdapter myData =
new SqlDataAdapter("SELECT ProductName, UnitPrice, UnitsInStock, " +
"Discontinued, CategoryID FROM Products order by ProductName", myConn);
DataSet ds = new DataSet();
myData.Fill(ds);
DataGrid1.DataSource = ds.Tables[0].DefaultView;
DataGrid1.DataBind();
}
void DataGrid1_ItemDataBound(object sender,
System.Web.UI.WebControls.DataGridItemEventArgs e)
{
if(e.Item.ItemType == ListItemType.Item ||
e.Item.ItemType == ListItemType.AlternatingItem)
{
switch((int)((DataRowView) e.Item.DataItem)["CategoryID"])
{
case 1:
e.Item.BackColor = Color.Aqua;
break;
case 2:
e.Item.BackColor = Color.Yellow;
break;
case 3:
e.Item.BackColor = Color.FloralWhite;
break;
case 4:
e.Item.BackColor = Color.Gold;
break;
default:
e.Item.BackColor = Color.White;
break;
}
}
}
</script>
<HEAD>
<title>DataGridExample1</title>
</HEAD>
<body MS_POSITIONING="FlowLayout">
<form method="post" runat="server" ID="Form1">
<asp:DataGrid id="DataGrid1" runat="server"
OnItemDataBound="DataGrid1_ItemDataBound">
</asp:DataGrid>
</form>
</body>
</HTML>
Теперь неплохо бы определиться с некоторыми важными моментами, на
которые я вскользь обратил внимание в прошлом примере. Первый момент –
тип объекта, содержащего данные. Как вы помните свойство
DataGridItemEventArgs.Item.DataItem имеет тип object и перед
использованием данное свойство необходимо привести к правильному типу.
Рассмотрим вкратце какие типы будут считаться «правильными» в конкретных
случаях.
Тип объекта – источника данных |
Тип объекта DataGridItemEventArgs.Item.DataItem |
DataView, DataTable |
System.Data.DataRowView |
IDataReader (SqlDataReader, OleDbDataReader) |
System.Data.Common.DbDataRecord |
Типизированная коллекция/массив объектов |
Тип объекта в коллекции (например для
StringCollection – string, для DataTableCollection – DataTable) |
Эти типы нужны вам для получения доступа к данным.
Также крайне важно знать какого типа строка в DataGrid/DataList
сейчас связывается. Как я уже упоминал, некоторые типы строк не имеют
объекта с данными (что часто бывает очень критично). Типы строк задаются
перечислением ListItemType и могут принимать следующие значения:
AlternatingItem |
Четная строка с данными |
DataGrid, DataList, Repeater |
EditItem |
Строка с редактируемыми данными |
DataGrid, DataList |
Footer |
Дно элемента управления |
DataGrid, DataList, Repeater |
Header |
Заголовок элемента управления |
DataGrid, DataList, Repeater |
Item |
Строка с данными |
DataGrid, DataList, Repeater |
Pager |
Строка, отображающая элементы управления для
постраничного просмотра данных |
DataGrid |
SelectedItem |
Выбранная строка с данными |
DataGrid, DataList |
Separator |
Разделитель между строками с данными |
DataList, Repeater |
Теперь вооружившись этими знаниями рассмотрим следующий пример: в
элементе управления DataGrid отобразим информацию из таблицы продуктов
базы Northwind с помощью SqlDataReader. Дополнительно также выведем
стоимость товара на складе для каждого товара и итоговую сумму. Для
неактивных товаров в соответствующей ячейке поставим символ X, иначе
пробел. Также пронумеруем выводимые в DataGrid строки (удивительно как
любит это начальство :)). Ну и чтобы проверить работу на максимальном
количестве типов строк установим выбранную и редактируемую строки в
значение, отличное от -1. А также заодно посмотрим на работу с
элементами управления в ячейках (для столбца «On Order» программно
заменим поле ввода, выводящееся по умолчанию для редактирования данных,
на выпадающий список).
В классе формы описаны 2 переменные, используемые для нумерации строк
(protected int num = 1;) и для суммы товаров на складе (private decimal
total = 0;). Пришла пора рассмотреть код примера.
Код элемента управления DataGrid веб формы:
<asp:DataGrid id="DataGrid1" runat="server"
AutoGenerateColumns="False" ShowFooter="True">
<Columns>
<asp:TemplateColumn HeaderText="#">
<HeaderStyle HorizontalAlign="Center"></HeaderStyle>
<ItemStyle HorizontalAlign="Center"></ItemStyle>
<ItemTemplate>
<%# num%>
</ItemTemplate>
</asp:TemplateColumn>
<asp:BoundColumn DataField="ProductName"
HeaderText="Product"></asp:BoundColumn>
<asp:BoundColumn DataField="QuantityPerUnit"
HeaderText="Quantity/Unit"></asp:BoundColumn>
<asp:BoundColumn DataField="UnitPrice" HeaderText="Price">
<ItemStyle HorizontalAlign="Right"></ItemStyle>
</asp:BoundColumn>
<asp:BoundColumn DataField="UnitsInStock" HeaderText="In Stock">
<ItemStyle HorizontalAlign="Right"></ItemStyle>
</asp:BoundColumn>
<asp:BoundColumn DataField="UnitsOnOrder" HeaderText="On Order">
<ItemStyle HorizontalAlign="Right"></ItemStyle>
</asp:BoundColumn>
<asp:TemplateColumn HeaderText="Total In Stock">
<ItemStyle HorizontalAlign="Right"></ItemStyle>
<ItemTemplate>
<asp:Label ID="lblTotal" Runat="server" />
</ItemTemplate>
<FooterStyle HorizontalAlign="Right"></FooterStyle>
</asp:TemplateColumn>
<asp:TemplateColumn HeaderText="Discontinued">
<ItemStyle HorizontalAlign="Center"></ItemStyle>
</asp:TemplateColumn>
</Columns>
</asp:DataGrid>
Код обработчика события ItemDataBound веб формы:
private void DataGrid1_ItemDataBound(object sender,
System.Web.UI.WebControls.DataGridItemEventArgs e)
{
System.Data.Common.DbDataRecord rec = null;
if(e.Item.ItemType ==ListItemType.Item ||
e.Item.ItemType == ListItemType.AlternatingItem ||
e.Item.ItemType == ListItemType.SelectedItem ||
e.Item.ItemType == ListItemType.EditItem)
rec = (System.Data.Common.DbDataRecord) e.Item.DataItem;
switch(e.Item.ItemType)
{
case ListItemType.Item:
case ListItemType.AlternatingItem:
case ListItemType.SelectedItem:
num++;
e.Item.Cells[7].Text = (bool) rec["Discontinued"] ? "X" : " ";
((Label) e.Item.FindControl("lblTotal")).Text =
((decimal) rec["UnitPrice"] * (short) rec["UnitsInStock"]).ToString();
total += (decimal) rec["UnitPrice"] * (short) rec["UnitsInStock"];
break;
case ListItemType.EditItem:
num++;
e.Item.Cells[7].Text = (bool) rec["Discontinued"] ? "X" : " ";
((Label) e.Item.FindControl("lblTotal")).Text =
((decimal) rec["UnitPrice"] * (short) rec["UnitsInStock"]).ToString();
e.Item.Cells[5].Controls.Clear();
DropDownList ddlOrder = new DropDownList();
ddlOrder.Items.Add(new ListItem("Select", ""));
ddlOrder.Items.Add(new ListItem("0", "0"));
ddlOrder.Items.Add(new ListItem("10", "10"));
ddlOrder.Items.Add(new ListItem("20", "20"));
ddlOrder.Items.Add(new ListItem("30", "30"));
ddlOrder.Items.Add(new ListItem("40", "40"));
ddlOrder.Items.Add(new ListItem("50", "50"));
ddlOrder.Items.Add(new ListItem("60", "60"));
ddlOrder.Items.Add(new ListItem("70", "70"));
ddlOrder.Items.Add(new ListItem("80", "80"));
ddlOrder.Items.Add(new ListItem("90", "90"));
ddlOrder.Items.Add(new ListItem("100", "100"));
if(ddlOrder.Items.FindByValue(rec["UnitsOnOrder"].ToString()) != null)
ddlOrder.Items.FindByValue(rec["UnitsOnOrder"].ToString()).Selected = true;
e.Item.Cells[5].Controls.Add(ddlOrder);
total += (decimal) rec["UnitPrice"] * (short) rec["UnitsInStock"];
break;
case ListItemType.Footer:
e.Item.Cells[5].Text = "<b>Total:</b>";
e.Item.Cells[5].HorizontalAlign = HorizontalAlign.Right;
e.Item.Cells[6].Text = String.Format("{0:c}", total);
e.Item.Cells[6].HorizontalAlign = HorizontalAlign.Right;
break;
}
}
Рассморим приведенный код подробнее. Для начала нам необходимо
получить объект с данными текущей строки. Но так как эти данные
существуют далеко не всегда – необходимо предварительно проверить какого
типа строка сейчас обрабатывается.
Затем в операторе switch происходит реальная работа с данными. Я
специально использовал именно конструктор switch для того, чтобы вы
могли закомментировав ту или иную часть кода увидеть изменения в
выполнении страницы.
Код для строк типа Item, AlternatingItem и SelectedItem достаточно
прост и не представляет особого интереса. Более интересные действия
происходят при обработке строки редактирования, а конкретно код для
вывода выпадающего списка вместо поля ввода для столбца UnitsOnOrder.
Для начала в соответствующей ячейке очищается список элементов
управления (и удаляется уже находящийся там элемент управления TextBox).
Затем создается и заполняется значениями элемент управления
DropDownList, а также выполняется код для выбора текущего значения поля
UnitsOnOrder. И наконец созданный элемент управления добавляется в
список элементов управления ячейки.
Последние 6 строк оператора switch добавляют данные о сумме товаров
на складе в дно элемента управления DataGrid, а также производят
минимальное форматирование выводимых данных.
Использование ItemCreated
Как я уже упоминал событие ItemDataBound срабатывает после связывания
данных, но до их вывода. Но бывают случаи, когда необходимо произвести
некоторые действия до связывания данных. И в этом случае на помощь
приходит событие ItemCreated, срабатывающее после создания строки, но до
связывания с данными.
Рассмотрим следующий пример: необходимо вывести информацию о клиентах
(таблица Customers) вместе с общей информацией о заказах клиентов
(таблица Orders и сумма товаров в заказе из таблицы Order Details базы
Northwind). Естесственно пронумеровать строки (как клиентов, так и
заказов клиентов), по каждому клиенту посчитать и вывести сумму всех
заказов, ну и чтобы не скучно было еще и выделить заказы на сумму
больше, чем 2500 :).
На первый взгляд все просто - выводим Repeater с информацией о
клиенте и там же вставляем DataGrid со списком заказов. Но возникают
некоторые «но»... Например добавление обработчика ItemDataBound к
элементу управления DataGrid, находящемуся в шаблоне Repeaterа,
определение источника данных для DataGrid и обнуление счетчика строк в
том же DataGrid с информацией о заказах.
Использовать для этого обработчик события ItemDataBound элемента
управления Repeater несколько поздновато – в этот момент все данные уже
связаны. Так что ItemCreated – именно то событие, которое нам нужно
(конечно можно ItemDataBound и источник данных для DataGrid можно задать
и по другому, но мы то сейчас рассматриваем работу с событием
ItemCreated :)).
Итак взглянем вначале на код веб формы:
<asp:Repeater id="Repeater1" runat="server">
<HeaderTemplate>
<table ID="Table4">
</HeaderTemplate>
<FooterTemplate>
</table>
</FooterTemplate>
<ItemTemplate>
<tr>
<td rowspan="2" valign="center"><%# num1 %></td>
<td>Company:
<%# DataBinder.Eval(Container.DataItem, "CompanyName")%>
</td>
<td>Contact:
<%# DataBinder.Eval(Container.DataItem, "ContactName")%>
</td>
<td>Address:
<%# DataBinder.Eval(Container.DataItem, "City")%>
,
<%# DataBinder.Eval(Container.DataItem, "PostalCode")%>
<%# DataBinder.Eval(Container.DataItem, "Country")%>
</td>
</tr>
<tr>
<td colspan="3" align="right">
<asp:DataGrid ID="DataGrid1" Runat="server"
AutoGenerateColumns="False" Width="80%" ShowFooter="True">
<Columns>
<asp:TemplateColumn HeaderText="#" ItemStyle-HorizontalAlign="Center">
<ItemTemplate>
<%# num2 %>
</ItemTemplate>
</asp:TemplateColumn>
<asp:BoundColumn HeaderText="Order ID" DataField="OrderID" />
<asp:BoundColumn HeaderText="Order date" DataField="OrderDate" />
<asp:BoundColumn HeaderText="Freight" DataField="Freight" />
<asp:BoundColumn HeaderText="Shipped Date" DataField="ShippedDate" />
<asp:BoundColumn HeaderText="Total" DataField="summ" />
</Columns>
</asp:DataGrid>
</td>
</tr>
</ItemTemplate>
</asp:Repeater>
Ничего принципиально сложного в этом коде нет, так что смело можно
его не расшифровывать. Более интересен код класса веб формы и я, на
всякий случай, приведу его весь:
public class ItemDataBoundExample3 : System.Web.UI.Page
{
protected System.Web.UI.WebControls.Repeater Repeater1;
protected int num1 = 0;
protected int num2 = 0;
private decimal total = 0;
DataSet ds = null;
private void Page_Load(object sender, System.EventArgs e)
{
SqlConnection myConn = new SqlConnection("server=localhost;database=Northwind;uid=sa;pwd=");
SqlDataAdapter myData = new SqlDataAdapter("SELECT
top 20 CustomerID, CompanyName, ContactName, " +
"City, PostalCode, Country FROM Customers order by CustomerID", myConn);
ds = new DataSet();
myData.Fill(ds, "Customers");
myData.SelectCommand.CommandText =
"SELECT Orders.CustomerID, Orders.OrderID, Orders.OrderDate, " +
"Orders.Freight, Orders.ShippedDate,
SUM([Order Details].UnitPrice * [Order Details].Quantity) " +
"AS summ FROM Orders INNER JOIN [Order Details]
ON Orders.OrderID = [Order Details].OrderID " +
"where CustomerID in (SELECT top 20 CustomerID
from Customers order by CustomerID) " +
"GROUP BY Orders.OrderID, dbo.Orders.OrderDate,
dbo.Orders.Freight, dbo.Orders.ShippedDate, " +
"dbo.Orders.CustomerID";
myData.Fill(ds, "Orders");
Repeater1.DataSource = ds.Tables["Customers"].DefaultView;
Repeater1.DataBind();
}
#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
InitializeComponent();
base.OnInit(e);
}
private void InitializeComponent()
{
this.Repeater1.ItemCreated += new
System.Web.UI.WebControls.RepeaterItemEventHandler(this.Repeater1_ItemCreated);
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
private void Repeater1_ItemCreated(object sender,
System.Web.UI.WebControls.RepeaterItemEventArgs e)
{
if(e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
{
num1++;
num2 = 1;
total = 0;
DataGrid dg = (DataGrid) e.Item.FindControl("DataGrid1");
dg.ItemDataBound +=
new System.Web.UI.WebControls.DataGridItemEventHandler(this.DataGrid1_ItemDataBound);
ds.Tables["Orders"].DefaultView.RowFilter =
String.Format("CustomerID = '{0}'", ((DataRowView) e.Item.DataItem)["CustomerID"]);
dg.DataSource = ds.Tables["Orders"].DefaultView;
}
}
private void DataGrid1_ItemDataBound(object sender,
System.Web.UI.WebControls.DataGridItemEventArgs e)
{
if(e.Item.ItemType == ListItemType.Item
e.Item.ItemType == ListItemType.AlternatingItem)
{
num2++;
total += (decimal) ((DataRowView) e.Item.DataItem)["summ"];
if((decimal) ((DataRowView) e.Item.DataItem)["summ"] > 2500)
e.Item.BackColor = Color.LightCoral;
}
if(e.Item.ItemType == ListItemType.Footer)
{
e.Item.Cells.Clear();
TableCell cell = new TableCell();
cell.ColumnSpan = 6;
cell.HorizontalAlign = HorizontalAlign.Center;
cell.Text = String.Format("Total: <b>{0}</b> usd", total);
e.Item.Cells.Add(cell);
}
}
}
В классе описаны 4 переменные. Num1 и num2 используются для нумерации
клиентов и заказов клиентов соответственно, переменная total
предназначена для получения общей суммы заказов клиента, ну а в ds
хранятся данные из таблиц Customers и Orders (в данном случае лучше
получить эти данные сразу используя 2 запроса и не мучать базу :)).
В Page_Load переменная ds заполняется данными из источника данных и
данные из таблицы Customers связываются с элементом Repeater. После чего
начинается самое интересное :). В обработчике события ItemCreated
элемента Repeater инициализируются переменные num2 и total для нумерации
строк в элементе DataGrid и подсчета суммы заказов клиента и для
элемента DataGrid, находящегося в текущей строке, программно добавляется
обработчик события ItemDataBound и источник данных. Обратите внимание на
то, что я не вызываю метод DataGrid.DataBind() для связывания данных,
так как это метод буде автоматически вызван при связывании строки
элемента Repeater.
Код в последнем обработчике, ItemDataBound для элемента DataGrid,
нашего примера не представляет особого интереса. Единственное, на что я
бы хотел обратить внимание – манипуляция с содержимым дна элемента
DataGrid, которая производится в условии if(e.Item.ItemType ==
ListItemType.Footer). Это код показывает как можно добавлять и удалять
ячейки в строке данных элемента DataGrid.
Использование событий ItemCreated/ItemDataBound дает программистам
мощные средства для управления содержанием и внешним видом выводимых
табличных данных.