В своей
предыдущей статье
Редактирование данных в DataGrid я как-то обошел стороной вопрос
связи между строками в DataGrid/DataList и данными в источнике данных, и
у читателей могло сложиться впечатление, что такоую связь им нужно
реализовывать самим. Но это не так - на самом деле такая связь
существует, и создается она с помощью свойств DataKeyField/DataKeys
элементов управления DataGrid и DataList. Все, что нам нужно для того,
чтобы привязать к строкам, связываемым в DataGrid, с информацию о
ключах этих строк из источника данных, это перед связыванием данных
присвоить имя необходимого нам ключевого поля в свойство DataKeyField:private void bindData()
{
SqlConnection myConn = new SqlConnection(ConnStr);
SqlCommand myCmd = new SqlCommand("select * from titles", myConn);
myConn.Open();
IDataReader rdr = myCmd.ExecuteReader();
DataGrid1.DataSource = rdr;
DataGrid1.DataKeyField = "title_id";
DataGrid1.DataBind();
rdr.Close();
myConn.Close();
}
Теперь мы можем получить значение ключа для указанной строки из
массива объектов DataKeys. Обратите внимание, что это свойство имеет тип
object[] и при получении значения ключа из этого массива необходимо
привести его к правильному типу данных.
Рассмотрим работу со свойством DataKeys на примере элемента
управления DataGrid, выводящего список книг из базы Pubs и
предоставляющего возможность удалять книги из базы. Метод для связывания
данных у нас уже есть, осталось посмотреть на описание самого DataGrid:
<asp:DataGrid id=DataGrid1 runat="server" AutoGenerateColumns="False">
<Columns>
<asp:BoundColumn DataField="title" HeaderText="Title"></asp:BoundColumn>
<asp:BoundColumn DataField="price" HeaderText="Price"></asp:BoundColumn>
<asp:BoundColumn DataField="pubdate" HeaderText="Pub Date"></asp:BoundColumn>
<asp:ButtonColumn Text="Delete" CommandName="Delete"></asp:ButtonColumn>
</Columns>
</asp:DataGrid>
и обработчика события DeleteCommand для него
private void DataGrid1_DeleteCommand(object source, System.Web.UI.WebControls.DataGridCommandEventArgs e)
{
SqlConnection myConn = new SqlConnection(ConnStr);
SqlCommand myCmd = new SqlCommand("delete from titles where title_id = @title_id", myConn);
myCmd.Parameters.Add("@title_id", DataGrid1.DataKeys[e.Item.ItemIndex]);
// myConn.Open();
// myCmd.ExecuteNonQuery();
// myConn.Close();
lblDeleted.Text = "delete title with title_id = 'э" + (string) DataGrid1.DataKeys[e.Item.ItemIndex] + "'";
bindData();
}
Я не делал реального удаления записи из БД, но если вам это нужно -
просто откомментируйте закомменченый участок.
Ну и чтобы 2 раза не вставать я покажу в этом же примере как к кнопке
удаления прицепить клиентский алерт, запрашивающий подтверждение
пользователя на удаление строки. Делается это в обработчике события
ItemDataBound с помощью следующего (верного для данного случая) кода:
private void DataGrid1_ItemDataBound(object sender, System.Web.UI.WebControls.DataGridItemEventArgs e)
{
if(e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
((LinkButton) e.Item.Cells[3].Controls[0]).Attributes.Add("onclick", "return confirm('Are you sure?');");
}
Для того, чтобы это код работал, необходимо определить индекс столбца
Delete, и подставить его в e.Item.Cells[нужный_номер_столбца].
Еще один интересный момент редактирования данных, который мне
хотелось бы рассмотреть в этой статье, это редактирование данных в
табличных элементах управления, вложенных в другие табличные элементы
управления. В этом случае возникают очень интересные проблемы, не
позволяющие использовать стандартный подход. Ведь при связывании данных
с внешним табличным элементом управления все строки этого элемента
управления пересоздаются заново и, соответственно, заново создаются и
все вложенные табличные элементы управления с данными.
Соответственно для реализации редактирования данных в этом
случае необходимо дополнительно сохранять 2 значения - номер строки
внешнего табличного элемента управления, в которой содержится табличный
элемент управления с редактируемой строкой, и номер самой редактируемой
строки во внутреннем табличном элементе управления. Попробуем
реализовать это.
Итак требуется создать страницу, выводящую список работников по
издательствам с возможностью редактирования данных работников. Внешний
список (издательств) формируется с помощью элемента управления Repeater,
внутренний же список (работников) - с помощью элемента управления
DataGrid. Для элемента управления DataGrid определены также обработчики
событий EditCommand, UpdateCommand и CancelCommand. Полный код веб формы
представлен ниже:
<form id="IncludedDataGridEdit" method="post" runat="server">
<P><asp:Label id=lblSQLCmd runat="server" EnableViewState="False"></asp:Label></P>
<asp:Repeater id=rptrPubs runat="server">
<ItemTemplate>
<b><%# DataBinder.Eval(Container.DataItem, "Pub_Name")%></b> employees:<br>
<asp:DataGrid ID="dgEmployees" Runat="server" AutoGenerateColumns="False" OnEditCommand="dgEmployees_EditCommand"
OnCancelCommand="dgEmployees_CancelCommand" OnUpdateCommand="dgEmployees_UpdateCommand">
<Columns>
<asp:BoundColumn DataField="fname" HeaderText="First Name"></asp:BoundColumn>
<asp:BoundColumn DataField="lname" HeaderText="Last Name"></asp:BoundColumn>
<asp:EditCommandColumn ButtonType="LinkButton" UpdateText="Update"
CancelText="Cancel" EditText="Edit"></asp:EditCommandColumn>
</Columns>
</asp:DataGrid>
</ItemTemplate>
<SeparatorTemplate>
<hr width="50%">
</SeparatorTemplate>
</asp:Repeater>
</form>
Как же будет работать эта форма? Изначально данные выводятся в
табличных элементах управления в режиме просмотра, но во вложенных
DataGridах в каждой строке присутствует кнопка Edit. При нажатии на эту
кнопку в обработчике события DataGrid.EditCommand в ViewState
сохраняются 2 переменные - индекс строки элемента управления DataGrid, в
котором произошло данное событие:
ViewState["selectedDataGridIndex"] = e.Item.ItemIndex;
а также индекс строки элемента управления Repeater, в которой лежит
данный DataGrid:
ViewState["selectedRepeaterIndex"] = ((RepeaterItem) ((DataGrid)
sender).Parent).ItemIndex;
после чего вызывается метод связывания элемента управления Repeater с
источником данных
Как же используются данные индексы для отображения строки в режиме
редактирования? Как я уже говорил ранее процедура связывания данных
пересоздает заново все элементы управления, вложенные в табличный
элемент управления, для которого был вызван метод DataBind. Но так как
нужные индексы строк уже есть - все, что необходимо сделать, это в
обработчике события ItemCreated(ItemDataBound) внешнего элемента
управления Repeater отловить по сохраненному ранее индексу нужную
строку и установить для вложенного в эту строку элемента управления
DataGrid значение свойства EditItemIndex в ранее сохраненное:
private void rptrPubs_ItemDataBound(object sender, System.Web.UI.WebControls.RepeaterItemEventArgs e)
{
if(e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
{
if((int) ViewState["selectedRepeaterIndex"] == e.Item.ItemIndex)
((DataGrid) e.Item.FindControl("dgEmployees")).EditItemIndex = (int) ViewState["selectedDataGridIndex"];
DataView dv = new DataView(ds.Tables[1]);
dv.RowFilter = "pub_id = " + ((DataRowView) e.Item.DataItem)["pub_id"].ToString();
((DataGrid) e.Item.FindControl("dgEmployees")).DataKeyField = "emp_id";
((DataGrid) e.Item.FindControl("dgEmployees")).DataSource = dv;
((DataGrid) e.Item.FindControl("dgEmployees")).DataBind();
}
}
Теперь осталось реализовать обработчики для событий отмены
редактирования и сохранения данных и дело сделано.
Обработчик события отмены редактирования (DataGrid.CancelCommand)
всего лишь сбразывает переменные, в которых хранятся индексы
редактируемых строк:
ViewState["selectedDataGridIndex"] = -1;
ViewState["selectedRepeaterIndex"] = -1;
после чего вызывается метод связывания данных.
Обработчик события сохранения редактируемых данных
(DataGrid.UpdateCommand) ненамного сложнее - его работа заключается в
сохранении сделанных изменений в базу. А так как в метод - обработчик
этого события передается полная информация о строке, в которой возникло
это событие, этот процесс ничем не отличается от
стандартного. В рассматриваемом примере в результате выполнения этого
обработчика выводится SQL запрос, который мог быть использован для
изменения данных в БД. И после изменения данных вызывается
метод-обработчик отмены редактирования.
lblSQLCmd.Text = String.Format("update employee set fname = '{0}', lname = '{1}' where emp_id = {2}",
((TextBox) e.Item.Cells[0].Controls[0]).Text, ((TextBox) e.Item.Cells[1].Controls[0]).Text,
((DataGrid) sender).DataKeys[e.Item.ItemIndex]);
this.dgEmployees_CancelCommand(sender, e);
Вот и все, веб форма, позволяющая редактировать данные во вложенных
табличных элементах управления, готова.
Теперь перейдем к рассмотрению события ItemCommand. Рассмотрим
следующий пример - есть элемент управления DataGrid с 2-мя Templated
столбцами, содержащими элементы управления LinkButton. Для одного
элемента управления LinkButton определен обработчик события OnClick, для
второго же указан CommandName. Также для самого DataGrid определен
обработчик события ItemCommand:
<asp:DataGrid id=DataGrid1 runat="server" AutoGenerateColumns="False">
<Columns>
<asp:TemplateColumn>
<ItemTemplate>
<asp:LinkButton Runat="server" OnClick="btn_click">Button <%# (int) Container.DataItem%></asp:LinkButton>
</ItemTemplate>
</asp:TemplateColumn>
<asp:TemplateColumn>
<ItemTemplate>
<asp:LinkButton Runat="server" CommandName="<%# Container.DataItem%>">Item <%# (int) Container.DataItem%>
</asp:LinkButton>
</ItemTemplate>
</asp:TemplateColumn>
</Columns>
</asp:DataGrid>
protected void btn_click(object sender, EventArgs e)
{
Response.Write("Button clicked<br>");
}
private void DataGrid1_ItemCommand(object source, System.Web.UI.WebControls.DataGridCommandEventArgs e)
{
Response.Write("Item fired<br>");
}
Обработчики событий Button.Click и DataGrid.ItemCommand просто
выводят сообщение о том, что они вызывались. Протестируем теперь даннуй
страницу - свяжем DataGrid с массивом целых и попробуем понажимать на
линки и посмотреть результаты.
При нажатии на линки из второго столбца срабатывает событие
DataGrid.ItemCommand и на страницу выводится надпись Item fired. Почему
это происходит и как это работае я расскажу несколько позже. Более
интересные результаты получаются если нажать на ссылку в первом столбце,
для которой у нас есть обработчик события OnClick. В этом случае
выводится надпись Button clicked (как в принципе и ожидалось :)), но
кроме нее также выводится и надпись Item fired, что означает что и в
этом случае обработчик события OnItemCommand отработал! И это важно
знать - обработчик события OnItemCommand срабатывает всегда, когда
нажимается кнопка, находящаяся внутри табличного элемента управления.
Как же это все работает? А с помощью всплывающих событий, т.е.
событий, передающихся вверх по иерархии элементов управления до тех пор,
пока какой-нибудь элемент управления не прекратит это всплывание. Работу
всплывающих событий поддерживают методы OnBubbleEvent и
RaiseBubbleEvent, определенные в классе System.Web.UI.Control - корневом
классе иерархии элементов управления ASP.NET. Детально рассматривать их
в этой статье я не буду, замечу лишь, что если вы не хотите, чтобы ваш
элемент управления не передавал свои события родителю (не поддерживал
всплывающие события) - переопределите метод OnBubbleEvent чтобы он
возвращал false.
Какую же пользу можно извлечь из этого всего? Представим себе
следующую задачу - необходимо вывести список издательств из базы данных
pubs и дать возможность на той же странице смотреть список
выпущенных выбранным издательством книг или список работников для этого
издательства. Для вывода списка издательств будет использоваться элемент
управления DataList. Разместим в шаблоне ItemTemplate этого элемента
управления информацию об издательстве (название издательства и
информацию о его местонахождении), а также 2 элемента управления
LinkButton с установленными атрибутами CommandName:
<ItemTemplate>
<%# DataBinder.Eval(Container.DataItem, "pub_name")%><br>
<%# DataBinder.Eval(Container.DataItem, "city")%>,
<%# DataBinder.Eval(Container.DataItem, "state")%>
<%# DataBinder.Eval(Container.DataItem, "country")%><br>
<asp:LinkButton CommandName="ViewTitles" Runat="server">View Titles</asp:LinkButton>
<asp:LinkButton CommandName="ViewEmployees" Runat="server">View Employees</asp:LinkButton>
</ItemTemplate>
Для вывода информации о работниках или книгах издательства будет
использоваться шаблон SelectedItemTemplate, который содержит кроме
элементов управления из шаблона ItemTemplate также LinkButton для
скрытия развернутой информации о работниках или книгах, а также элемент
управления DataGrid для вывода этой информации. Для простоты я
использовал минимально функциональный DataGrid с атрибутом
AutoGenerateColumns установленном в true:
<SelectedItemTemplate>
<%# DataBinder.Eval(Container.DataItem, "pub_name")%><br>
<%# DataBinder.Eval(Container.DataItem, "city")%>,
<%# DataBinder.Eval(Container.DataItem, "state")%>
<%# DataBinder.Eval(Container.DataItem, "country")%><br>
<asp:LinkButton CommandName="ViewTitles" Runat="server">View Titles</asp:LinkButton>
<asp:LinkButton CommandName="ViewEmployees" Runat="server">View Employees</asp:LinkButton>
<asp:LinkButton CommandName="Hide" Runat="server">Hide</asp:LinkButton><br>
<asp:DataGrid ID="dgDetails" Runat="server" />
</SelectedItemTemplate>
Теперь осталось добавить код для отображения необходимых данных при
нажатии на соответствующие LinkButton. Для этого необходимо создать
обработчик события ItemCommand в котором в зависимости от значения
CommandName произвести следующие действия:
- Установить значение SelectedIndex элемента управления DataList в
значение номера строки, в которой произошло событие. Получить это
значение можно из свойства Item.ItemIndex класса
DataListCommandEventArgs, передаваемого в обработчик события (либо
же установить его в -1 если был нажат LinkButton c
CommandName="Hide"
- Сохранить значение CommandName для последующей обработки его в
обработчике события DataList.ItemDataBound (именно там будет
происходить реальный вывод данных о работниках или книгах
издательства).
- Создать обработчик события DataList.ItemDataBound в котором в
шаблоне SelectedItemTemplate связать необходимые данные с элементом
управления DataGrid, расположенном в нем.
Код класса страницы, выполяющий это, представлен ниже:
public class DataGridItemCmd : System.Web.UI.Page
{
protected System.Web.UI.WebControls.DataList dlPublishers;
private string ConnStr = "server=localhost;database=pubs;uid=sa;pwd=";
private string CmdName = "";
private void Page_Load(object sender, System.EventArgs e)
{
if(!IsPostBack)
{
bindData();
}
}
override protected void OnInit(EventArgs e)
{
InitializeComponent();
base.OnInit(e);
}
private void InitializeComponent()
{
this.dlPublishers.ItemCommand += new System.Web.UI.WebControls.DataListCommandEventHandler(this.dlPublishers_ItemCommand);
this.dlPublishers.ItemDataBound += new System.Web.UI.WebControls.DataListItemEventHandler(this.dlPublishers_ItemDataBound);
this.Load += new System.EventHandler(this.Page_Load);
}
private void bindData()
{
SqlConnection myConn = new SqlConnection(ConnStr);
SqlCommand myCmd = new SqlCommand("select * from publishers", myConn);
myConn.Open();
IDataReader dr = myCmd.ExecuteReader(CommandBehavior.CloseConnection);
dlPublishers.DataSource = dr;
dlPublishers.DataBind();
dr.Close();
}
private void dlPublishers_ItemCommand(object source, System.Web.UI.WebControls.DataListCommandEventArgs e)
{
switch(e.CommandName)
{
case "ViewTitles":
case "ViewEmployees":
dlPublishers.SelectedIndex = e.Item.ItemIndex;
CmdName = e.CommandName;
break;
case "Hide":
dlPublishers.SelectedIndex = -1;
break;
}
bindData();
}
private void dlPublishers_ItemDataBound(object sender, System.Web.UI.WebControls.DataListItemEventArgs e)
{
if(e.Item.ItemType == ListItemType.SelectedItem)
{
SqlDataAdapter myData;
if(CmdName == "ViewTitles")
myData = new SqlDataAdapter("select * from titles where pub_id = " + ((IDataRecord) e.Item.DataItem)["pub_id"].ToString(),
ConnStr);
else
myData = new SqlDataAdapter("select * from employee where pub_id = " + ((IDataRecord) e.Item.DataItem)["pub_id"].ToString(),
ConnStr);
DataSet ds = new DataSet();
myData.Fill(ds);
((DataGrid) e.Item.FindControl("dgDetails")).DataSource = ds.Tables[0].DefaultView;
((DataGrid) e.Item.FindControl("dgDetails")).DataBind();
}
}
}
Как видите событие ItemCommand позволяет с легкостью обрабатывать
события, возникающие при нажатии на элементы управления
Button/LinkButton, добавляемые в табличные элементы управления. При этом
в обработчик события ItemCommand кроме информации о том, какое событие
было сгенерировано (свойство CommandName класса
DataListCommandEventArgs) также передается ссылка на саму строку, в
которой произошло это событие и программист имеет всю необходимую ему
информацию.
С помощью события ItemCommand можно даже сделать редактируем элементу
управления Repeater, пример чего я и покажу напоследок реализовав
элементе управления repeater редактирование данных таблицы titles базы
данных pubs.
Сначала необходимо решить как будут отображаться и редактироваться
данные в нем. Для этого в шаблоне ItemTemplate создается один регион,
предназначенный для вывода данных в режиме просмотра, и второй для
вывода данных для редактирования. Оба этих региона имеют атрибут
runat="server" и их свойства Visible меняются в зависимости от того,
редактируется ли данная запись или просто выводится в режиме просмотра.
Также в этих регионах размещены элементы управления Button c атрибутами
CommandName, установленными в "edit" (для перехода в режим
редактирования), "update" (для сохранения редактируемой строки) и
"cancel" (для отмены редактирования). Полный код данного элемента
управления Repeater представлен ниже:
<asp:Repeater id=rptrMain runat="server">
<HeaderTemplate>
<table cellpadding="2" cellspacing="2" align="center">
<tr>
<td>Title</td>
<td>Price</td>
<td>Pub Date</td>
</tr>
</HeaderTemplate>
<FooterTemplate>
</table>
</FooterTemplate>
<ItemTemplate>
<tr id="view" runat="server">
<td><%# DataBinder.Eval(Container.DataItem, "title") %></td>
<td><%# DataBinder.Eval(Container.DataItem, "price") %></td>
<td><%# DataBinder.Eval(Container.DataItem, "pubdate") %></td>
<td><asp:Button Runat="server" Text="Edit" CommandName="edit" /></td>
</tr>
<tr id="edit" runat="server" visible="false">
<td><asp:TextBox id="txtTitleID" runat="server" Text='<%# DataBinder.Eval(Container.DataItem, "title_id") %>' Visible="False" />
<asp:TextBox id="txtTitle" runat="server" Text='<%# DataBinder.Eval(Container.DataItem, "title") %>' /></td>
<td><asp:TextBox id="txtPrice" runat="server" Text='<%# DataBinder.Eval(Container.DataItem, "price") %>'/></td>
<td><asp:TextBox id="txtPubDate" runat="server" Text='<%# DataBinder.Eval(Container.DataItem, "pubdate") %>'/></td>
<td><asp:Button Runat="server" Text="Update" CommandName="update" />
<asp:Button Runat="server" Text="Cancel" CommandName="cancel"/></td></tr>
</ItemTemplate>
</asp:Repeater>
Как видите для того, чтобы строку перевести в режим редактирования
нужно всего 2 строчки кода:
view.Visible = false;
edit.Visible = true;
Но где и когда это делать? А делать это придется в обработчике
события Repeater.ItemDataBound (или Repeater.ItemCreated, тут уж на
любителя), предварительно сохранив индекс строки, которая в данный
момент находится в режиме редактирования. Забегая несколько вперед
скажу, что индекс редактируемой строки будет храниться в
ViewState["EditItem"], и исходя из этого обработчик события
Repeater.ItemDataBound будет иметь следующий код:
if((int) ViewState["EditItem"] != -1 && e.Item.ItemIndex == (int) ViewState["EditItem"])
{
e.Item.FindControl("view").Visible = false;
e.Item.FindControl("edit").Visible = true;
}
Теперь осталось дело за малым - реализовать обработчик события
Repeater.ItemCommand, в котором будут обрабатываться события, посылаемые
кнопками, вставленными в шаблоны элемента управления Repeater. Напомню,
что необходимо обработать 3 типа событий - "edit", "cancel" и "update".
При возникновении события "edit" необходимо только сохранить индекс
строки, в которой произожло это событие, для ее перевода в режим
редактирования. Аналогично для "cancel" необходимо установить индекс
редактируемой строки в -1 (нет редактируемых строк). Больше работы
придется выполнить при событии "update" - в этом случае необходимо
получить измененные данные из текстовых элементов управления и обновить
их в источнике данных. После чего опять таки сбросить индекс
редактируемой строки. Ну и напоследок необходимо вызвать процедуру
связывания данных для отображения произошедших изменений.
Небольшое примечание - к сожалению элемент управления Repeater не
имеет свойств DataKeyField и DataKeys и поэтому придется вручную
сохранфть значение ключа даписи в строке с данными. Это делается путем
добавления невидимого элемента управления TextBox, содержащего значение
поля title_id.
private void rptrMain_ItemCommand(object source, System.Web.UI.WebControls.RepeaterCommandEventArgs e)
{
switch(e.CommandName)
{
case "edit":
ViewState["EditItem"] = e.Item.ItemIndex;
break;
case "update":
try
{
SqlConnection myConn = new SqlConnection(ConnStr);
SqlCommand myCmd = new SqlCommand("update titles set title = @title, price = @price, pubdate = @pubdate where " +
"title_id = @title_id", myConn);
myCmd.Parameters.Add("@title", ((TextBox) e.Item.FindControl("txtTitle")).Text);
myCmd.Parameters.Add("@price", Decimal.Parse(((TextBox) e.Item.FindControl("txtPrice")).Text));
myCmd.Parameters.Add("@pubdate", DateTime.Parse(((TextBox) e.Item.FindControl("txtPubDate")).Text));
myCmd.Parameters.Add("@title_id", ((TextBox) e.Item.FindControl("txtTitleID")).Text);
myConn.Open();
myCmd.ExecuteNonQuery();
myConn.Close();
}
catch {}
ViewState["EditItem"] = -1;
break;
case "cancel":
ViewState["EditItem"] = -1;
break;
}
bindData();
}
Вот и все. Осталось добавить метод bindData(), реализующий связывание
данных и не забыть инициализировать значение в ViewState["EditItem"].
После чего элемент управления Repeater с возможностью редактирования
данных готов. |