Saturday, September 12, 2009

Inplace Edit in GridView By Jay@Thakar.info

Introduction

In this article I am presenting a different way to look at EditItemTemplate, using which user navigation for editing is reduced and … it looks very different (see screeen shots) then regular implementation of GridView.

Inline Edit

Background

Few days back I got an assignment, I needed to add few new fields in a DataViewand let user edit those (along with all existing fields).

I thought it was a very small change, just new fields in ItemTemplate and EditItemTemplateand perform few basic validations on it using JavaScript and I m done !. I was dead wrong. As soon as I modified EditItemTemplate, a Horizontal scrollbar started to appear on page. One of UI rule in our entire application on minimum resolution 1024x678 and with default font size, no Horizontal Scrollbar should appear on any of page.So I started thinking and creating new concepts. Here we had few options.

Download Source   Download Source

Various Options

Option 1:
Create new page for edit. When user clicks on edit button we redirect user to new page and when user clicks on Update/Close we bring them back on list page.
Problem:It was lengthy process to create new page, validate and bring them back, Also in this case link between current work and edit was broken as user was moving back and forth for editing.

Option 2:
Create a popup window, when user clicks on edit button we create a popup window and let user edit in new window, when user clicks on update/close user comes back to list page.
Problem:
As modern design approach we were moving away from popup windows (which opens as new window) and tried to find some other alternate for this popup windows. While talking about this option I also threw AjaxControlToolkit Model Popup but it had same problem as option 1, link between current data and editing data was broken.

Option 3:
Create inline form which gets displayed between 2 rows in GridView, Let user make any modification required there and when user clicks on update/close bring back same row. This is not new technique but there are many companies who offer this kind of facility (and change big bucks for that) As this was bit easy to implement I thought let’s try creating it on our own (and save some buck, of course for company :) )
Problem: NONE !!!

My IT Director was very happy to see this prototype, She suggested that I should implement this style. And the journey begins …

There are few things which generates this facility.

As you already know GridView gets rendered as table and each column defination in GridViewgenerates one column; so we defined only one column. To create an illusion of Table we defined only one column and created a table in it; We created required columns in this new inner table. This gets rendered as one table per DataRow with all columns we specified.

<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" CellPadding="4"
ForeColor="#333333" GridLines="Both" Width="60%" OnRowEditing="GridView1_RowEditing">
<RowStyle BackColor="#EFF3FB" />
<Columns>
<asp:TemplateField>
...
...
<ItemTemplate>
<table width="100%" cellpadding="0" cellspacing="0" >
<tr>
<td style="width:15%;" align="left">
<%#DataBinder.Eval(Container.DataItem, "FirstName")%>
</td>
<td style="width:15%;" align="left">
<%#DataBinder.Eval(Container.DataItem, "LastName")%>
</td>
<td style="width:35%;" align="left">
<%#DataBinder.Eval(Container.DataItem, "Web")%>
</td>
<td style="width:35%;" align="left">
<%#DataBinder.Eval(Container.DataItem, "Email")%>
</td>
</tr>
</table>

</ItemTemplate>

Above ItemTemplate will generate one table per row. Now let's see how we can make inline editing possible.


<EditItemTemplate>
<center>
<ajx:RoundedCornersExtender ID="rce" runat="server" TargetControlID="pnlHead"
Radius="6" Corners="Top" BorderColor="black" />
<asp:Panel ID="pnlHead" runat="server" Font-Bold="true" ForeColor="white"
BackColor="#507CD1"
Width="50%">
<center>
<table>
<tr>
<td>
Updating
<%#DataBinder.Eval(Container.DataItem, "FirstName")%>,
<%#DataBinder.Eval(Container.DataItem, "LastName")%>
</td>
</tr>
</table>
</center>
</asp:Panel>
<ajx:RoundedCornersExtender ID="rceDetail" runat="server" TargetControlID="pnlDetail"
Radius="6" Corners="Bottom" BorderColor="black" />
<asp:Panel ID="pnlDetail" runat="server" Width="50%">
<table width="100%">
<tr>
<td align="right" style="width: 30%">
First Name:
</td>
<td style="padding-right:10px;">
<asp:TextBox ID="tbFirstName" Width="100%" runat="server"
Text='<%#DataBinder.Eval(Container.DataItem, "FirstName")%>'>
</asp:TextBox>
</td>
</tr>
<tr>
<td align="right">
Last Name:
</td>
<td style="padding-right:10px;">
<asp:TextBox ID="TextBox1" runat="server" Width="100%"
Text='<%#DataBinder.Eval(Container.DataItem, "LastName")%>'>
</asp:TextBox>
</td>
</tr>
<tr>
<td align="right">
Web:
</td>
<td style="padding-right:10px;">
<asp:TextBox ID="TextBox2" runat="server" Width="100%"
Text='<%#DataBinder.Eval(Container.DataItem, "Web")%>'>
</asp:TextBox>
</td>
</tr>
<tr>
<td align="right">
eMail:
</td>
<td style="padding-right:10px;">
<asp:TextBox ID="TextBox3" runat="server" Width="100%"
Text='<%#DataBinder.Eval(Container.DataItem, "eMail")%>'>
</asp:TextBox>
</td>
</tr>
</table>

<div style="padding-top: 3px; border-top: thin solid black;" id="divUpdate">
<asp:Button CssClass="btnNormal" runat="server" ID="bUpdate"
Text="Update" OnClick="bUpdate_Click" />    
<asp:Button OnClick="bCancel_Click" CssClass="btnNormal"
runat="server" ID="bCancel" Text="Cancel" />

</div>
</asp:Panel>
</center>
</EditItemTemplate>
</asp:TemplateField>

From our ItemTemplate it was clear that in edit mode we will get only one column, so just like in ItemTemplate here too we defined edit layout whichever way we want. In our case we wanted rounded corner center aligned table, with different style on header, to acheave this we used AjaxControlToolkit's RoundedCornerExtension, it has facility to define which corners we want as rounded corners so as header we defined only top corners as rounded and for content part we defined only bottom corners as rounded corners. And using table we defined a layout of form for editing. this gives us a nice looking form when user clicks on edit button. And as result we got following GridView.


Image 2: Edit View in inline Grid View

As final touch we also needed header to match given columns so we used same type of style we used on ItemTemplate.


<HeaderTemplate>
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="width:15%;" align="left">
First Name
</td>
<td style="width:15%;" align="left">
Last Name
</td >
<td style="width:35%;" align="left">
Web
</td>
<td style="width:35%;" align="left">
Email
</td>
</tr>
</table>
</HeaderTemplate>

Note:Please make sure that ItemTemplate table defination matches to header (specially width of each column)  as it will give one table look.

Update:After publishing this article I got several requests about few things ...

  1. Code to update data on click of Update button.
  2. Add paging and code to preserve edit while moving between pages in pageble GridView.
  3. Disable all Edit button in Edit state (so user can not 'by accident' update other row.

So here is code ...

1.Code to update data on click of Update button.

Main purpose for this article was to present different way to use ItemTemplate; I understand taht there may be few questions about updating data which is beyond of scope of this article. Here is sample code for how to access updated value when user clicks on Update Button.

protected void bUpdate_Click(object sender, EventArgs e)
{
TextBox tbF = (TextBox)GridView1.Rows[GridView1.EditIndex].FindControl("tbFirstName");
TextBox tbL = (TextBox)GridView1.Rows[GridView1.EditIndex].FindControl("tbLastName");
TextBox tbW = (TextBox)GridView1.Rows[GridView1.EditIndex].FindControl("tbWeb");
TextBox tbE = (TextBox)GridView1.Rows[GridView1.EditIndex].FindControl("tbEmail");

// now you have all values entered by user
// set those values in dataset and update
// OR generate UPDATE sql statement here and save values in db.
// more details / example can be found at
// http://msdn.microsoft.com/en-us/library/ms972948.aspx
// OR
// http://www.aspdotnetcodes.com/GridView_Insert_Edit_Update_Delete.aspx
refreshData();
}

private void refreshData()
{
GridView1.EditIndex = -1;
ViewState["EditRowID"] = null;
GridView1.DataSource = CreateTable();
GridView1.DataBind();
}

The trick here is to use FindControl on currently editing row and pass ID of control which you want to access. And to accesss currently editing row you can use GridView1.Rows[GridView1.EditIndex]. Once we are done updating data change EditIndex to -1 (indicating no row is in edit state); this will restore all rows of grid back to view state (i.e. hide edit template).

For more details on how to update data into database please refer to following link.

http://msdn.microsoft.com/en-us/library/ms972948.aspx

2. Add paging and code to preserve edit while moving between pages in pageble GridView.


private int iCurrentEdit = -1;
protected void GridView1_RowEditing(Object sender, GridViewEditEventArgs e)
{
GridView1.EditIndex = e.NewEditIndex;
GridView1.DataSource = CreateTable();
GridView1.DataBind();
ViewState.Add("editRowIndex", e.NewEditIndex);
ImageButton editButton;

foreach (GridViewRow row in GridView1.Rows)
{
if (row.RowIndex != e.NewEditIndex)
{
editButton = (ImageButton)(row.Cells[1].Controls[0]);
editButton.ImageUrl = "./edit_off.gif";
editButton.Enabled = false;
}
else
{
ViewState.Add("EditRowID", row.DataItemIndex.ToString());
if (ViewState["fname"] != null)
// indicates we have values from previous editing session which are not saved
{
// get elements from edit template
TextBox tbF = (TextBox)row.FindControl("tbFirstName");
TextBox tbL = (TextBox)row.FindControl("tbLastName");
TextBox tbW = (TextBox)row.FindControl("tbWeb");
TextBox tbE = (TextBox)row.FindControl("tbEmail");

// restore previous values
tbF.Text = ViewState["fname"].ToString();
tbL.Text = ViewState["lname"].ToString();
tbW.Text = ViewState["web"].ToString();
tbE.Text = ViewState["email"].ToString();
}

}
}
}

protected void GridView1_PageIndexChanging(Object objDGName, GridViewPageEventArgs e)
{

GridView1.DataSource = CreateTable();
GridView1.PageIndex = e.NewPageIndex;
GridView1.EditIndex = -1;
GridView1.DataBind();

if (iCurrentEdit != -1)
{
GridView1_RowEditing(GridView1, new GridViewEditEventArgs(iCurrentEdit));
}
}

protected void GridView1_RowDataBound(Object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.DataRow ||
e.Row.RowType == DataControlRowType.EmptyDataRow)
{
int iEditRowID = int.Parse((ViewState["EditRowID"] == null ? "-2" :
ViewState["EditRowID"].ToString()));
if (iEditRowID != -2 && e.Row.DataItemIndex == iEditRowID)

{
iCurrentEdit = e.Row.RowIndex;
}
else if (iEditRowID != -2)
{
ImageButton editButton;
editButton = (ImageButton)(e.Row.Cells[1].Controls[0]);
editButton.ImageUrl = "./edit_off.gif";
editButton.Enabled = false;
}
}
}

There are few things here ...

We defined a class level variable called iCurrentEdit with initial value of -1 indicating no row is in edit state.

When user clicks on Edit (protected void GridView1_RowEditing(Object sender, GridViewEditEventArgs e)) button we loop thrue entire grid to find DataItemIndex and when we find it we store DataItemIndex into ViewState using ViewState.Add("EditRowID", row.DataItemIndex.ToString());

Now when user clicks to change page (protected void GridView1_PageIndexChanging(Object objDGName, GridViewPageEventArgs e)) first we call GridView1.DataBind(); which will call RowDataBound for each row.

Finally in RowDataBound event (protected void GridView1_RowDataBound(Object sender, GridViewRowEventArgs e)) we retrive value of currently editing DataItemIndex (int iEditRowID = int.Parse((ViewState["EditRowID"] == null ? "-2" : ViewState["EditRowID"].ToString()));) and then we compare this value against current row's DataItemIndex, If it is same then this indicates this is row in edit state hence we store RowIndex into iCurrentEdit so; when if (iCurrentEdit != -1) gets executed in PageIndexChanging there will be value in iCurrentEdit (if a row is in edit state) if this is the case then we call GridView1_RowEditing(GridView1, new GridViewEditEventArgs(iCurrentEdit)); to simulate rowEditClick.

In above code there is one potential problem, If user clicks on edit and changes First Name (or any other field in that matter); does not click on update button and goes to other page to view some information and comes back, above code will take care of putting correct row in edit mode but will not preserve values entered by user. To save those values temperarly we can use ViewState.


protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
GridView1.DataSource = CreateTable();
GridView1.DataBind();
}
else
{
if (ViewState["editRowIndex"] != null)
// indicates we have values from edit which needs to be saved in viewstate
{
int iEditRowID = int.Parse((ViewState["editRowIndex"] == null ? "-2" :
ViewState["editRowIndex"].ToString())); // get current edit row index

// get all elements from edit template
TextBox tbF = (TextBox)GridView1.Rows[iEditRowID].FindControl("tbFirstName");
TextBox tbL = (TextBox)GridView1.Rows[iEditRowID].FindControl("tbLastName");
TextBox tbW = (TextBox)GridView1.Rows[iEditRowID].FindControl("tbWeb");
TextBox tbE = (TextBox)GridView1.Rows[iEditRowID].FindControl("tbEmail");

// save all values into viewstate for future use
if (tbF != null)
{
ViewState.Add("fname", tbF.Text);
ViewState.Add("lname", tbL.Text);
ViewState.Add("web", tbW.Text);
ViewState.Add("email", tbE.Text);
}
}
}
}

private void refreshData()
{
GridView1.EditIndex = -1;
ViewState["EditRowID"] = null;
GridView1.DataSource = CreateTable();
GridView1.DataBind();

// clear viewstate (this is called from UpdateButtonClicked and CancelButtonClicked
ViewState["fname"] = null;
ViewState["lname"] = null;
ViewState["web"] = null;
ViewState["email"] = null;
ViewState["editRowIndex"] = null;

}

3. Disable all Edit button in Edit state (so user can not 'by accident' update other row.

When user clicks on edit button for any row; user can not (or should not) be able to click on edit button of any other row; for that we need to disable all edit buttons when user is in edit mode.


protected void GridView1_RowEditing(Object sender, GridViewEditEventArgs e)
{
...
...
ImageButton editButton;
foreach (GridViewRow row in GridView1.Rows)
{
if (row.RowIndex != e.NewEditIndex)
{
editButton = (ImageButton)(row.Cells[1].Controls[0]);
editButton.ImageUrl = "./edit_off.gif";
editButton.Enabled = false;

}
}
...
...
}
protected void GridView1_RowDataBound(Object sender, GridViewRowEventArgs e)
{
...
...
else if (iEditRowID != -2)
{
ImageButton editButton;
editButton = (ImageButton)(e.Row.Cells[1].Controls[0]);
editButton.ImageUrl = "./edit_off.gif";
editButton.Enabled = false;
}

}

Here when user clicks on Edit button we loop through entire grid and Disable edit button if it's index does not match with current edit row index. To access edit button we use editButton = (ImageButton)(e.Row.Cells[1].Controls[0]); as you already know we have created entire edit template in one table cell hence second cell (at cell index 1) will be for edit/update/cancel buttons and from that we access first control which is edit button; we disable it and change image icon to indicate this row is not available for editing. Almost similer thing we are doing on row bound which takes care of rows which are not on current page. When user goes from currently editing page to another page we wanted to disable editing which we took care in RowBound event.

History

Updated Code for various event on Sep 23, 2009.

Initial submission on Sep 14, 2009.

Download Source   Download Source

Summary

I hope this help you to create easy to use GridView

Happy Coding :)

3 comments:

  1. Thanks for the great tip and tutorial.

    Can you please provide some sample update code as I am having trouble getting the values from the controls in the panel.

    ReplyDelete
  2. Updated code for ...

    1. Code to update data on click of Update button.
    2. Add paging and code to preserve edit while moving between pages in pageble GridView.
    3. Disable all Edit button in Edit state (so user can not 'by accident' update other row.

    ReplyDelete
  3. Hi Friend,

    Great way to do inline editing in Gridview.
    But in my opnion I have implemented another way of editing data in Gridview without having horizontal scrollbar, and my Gridview has 13 columns, so obvious if I use EditItemTemplate it will show scrollbar.

    Problem I see in your design,
    As Gridview generates row as the rows returned by your database, and if in every row you will add the code specified your page size will be very heavy. We can do this by using ObjectDatasource and bit of HTML and javascript. I will post the code, screenshort and documnetation about it some time l8r.

    Thanks,
    Yash

    ReplyDelete