|
В своей предыдущей статье Редактирование
данных в 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 с возможностью редактирования данных готов.
|