Introduction
Recently, someone had asked in the forums how to use an ASP.NET Repeater control for adding new items to the datasource it is bound to. Since words sometimes don't convey enough understanding, I put together this sample to show how the Repeater control can be used in this situation along with in-place editing.
This sample contains two versions; a server-side oriented approach, and a client-side oriented approach using AJAX.
Repeater vs. Gridview
When providing in-place editing and adding rows, the first thing that comes to mind is usually to make use of an ASP.NET GridView control. This article is not about the differences between the two controls nor when/how to choose one over the other, but a quick look at the differences shows this:
| GridView |
Repeater |
| Table layout by default |
Uses templates |
| Has select/edit/delete commands |
Must be added manually |
| Built-in pager support |
Must be added manually |
| Column sorting |
Must be added manually |
Although not an exhaustive list, it seems as though the GridView holds a distinct advantage by providing more built-in functionality. However, all of this functionality comes at a price. The GridView is a very "heavy" control and relies on extensive use of ViewState to work correctly and in some cases adds too much overhead to a page. That is when an ASP.NET Repeater control can have advantages.
Server-side Approach
In this first approach, I'll rely on the traditional server-side coding using PostBacks and databinding events to provide the functionality. I'll wrap the Repeater control in and ASP.NET UpdatePanel to reduce the page refreshing, but otherwise there are only a few lines of client-side code involved.
<asp:UpdatePanel runat="server">
<ContentTemplate>
<asp:Repeater runat="server" ID="Repeater1"
OnItemCommand="OnItemCommand" OnItemDataBound="OnItemDataBound">
<HeaderTemplate>
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<th></th>
<th>First Name</th>
<th>Last Name</th>
</tr>
</HeaderTemplate>
<ItemTemplate>
<tr>
<td>
<asp:ImageButton ID="Edit"
ImageUrl="~/Images/EditDocument.png"
runat="server" CommandName="edit" />
<asp:ImageButton ID="Delete"
ImageUrl="~/Images/Delete_black_32x32.png" runat="server"
CommandName="delete" />
</td>
<td>
<asp:Label runat="server"
ID="firstName"><%# Eval
("FirstName") %></asp:Label>
<asp:PlaceHolder runat="server"
ID="firstNameEditPlaceholder" />
<input type="hidden" runat="
server" id="firstNameHidden" />
</td>
<td>
<asp:Label runat="server" ID="
lastName"><%# Eval("LastName") %></asp:Label>
<asp:PlaceHolder runat="server"
ID="lastNameEditPlaceholder" />
<input type="hidden" runat="
server" id="lastNameHidden" />
</td>
</tr>
</ItemTemplate>
<FooterTemplate>
<tr>
<td>
<asp:ImageButton ID="Delete"
ImageUrl="~/Images/112_Plus_Blue_32x32_72.png" runat="server"
OnClick="OnAddRecord" />
</td>
<td><asp:TextBox runat="server"
ID="NewFirstName" /></td>
<td><asp:TextBox runat="server"
ID="NewLastName" /></td>
</tr>
</table>
</FooterTemplate>
</asp:Repeater>
</ContentTemplate>
</asp:UpdatePanel>
As can be seen, this is a very minimal example, just enough to demonstrate the techniques, actual usage will, of course, vary. A table layout is used for simplicity but any type of layout can be produced using the templates and CSS.
The two key events here are OnItemCommand and OnItemDataBound which will be covered in a moment.
Data Source
Since the control needs a datasource in order to bind to, I've created a very simple entity...
public class Contact
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
...with a very simple Data Access Layer for this sample:
public class Data
{
public Data()
{
}
public int NextId
{
get
{
int id = 0;
if(Contacts.Count != 0)
{
id = Contacts.Max(c => c.ID) + 1;
}
return id;
}
}
public List<contact /> Contacts
{
get
{
if(HttpContext.Current.Session["contacts"] == null)
{
HttpContext.Current.Session["contacts"] = new List<contact />();
}
return HttpContext.Current.Session["contacts"] as List<contact />;
}
}
}
Of course, in a production system, this would be more complex and most likely use a Database as the datastore. However, for demonstration purposes, this will be sufficient.
Server-side Events
If no data is present for the control to bind to, there needs to be a way to add items to the datasource. The markup shown above contains a FooterTemplate with a table row containing controls to allow the user to input data and a LinkButton to trigger the OnAddRecored event handler.
protected void OnAddRecord(object sender, EventArgs e)
{
TextBox firstName = ((Control)sender).Parent.FindControl("NewFirstName") as TextBox;
TextBox lastName = ((Control)sender).Parent.FindControl("NewLastName") as TextBox;
if(!string.IsNullOrWhiteSpace(firstName.Text) ||
!string.IsNullOrWhiteSpace(lastName.Text))
{
Data.Contacts.Add(new Contact()
{ ID = Data.NextId, FirstName = firstName.Text, LastName = lastName.Text });
Repeater1.DataSource = Data.Contacts;
Repeater1.DataBind();
}
}
Nothing too complex here. The event is being triggered from the LinkButton, the sender object in this event handler. From that, you get the RepeaterItem which is the Parent object of the LinkButton and find the TextBox controls. Then it is simply a matter of adding to the datasource and rebinding the Repeater control. It gets slightly more complex when handling in-place editing.
The OnItemCommand event handles the command actions form the LinkButtons:
protected void OnItemCommand(object source, RepeaterCommandEventArgs e)
{
if(e.CommandName == "delete")
{
Data.Contacts.RemoveAt(e.Item.ItemIndex);
}
else if(e.CommandName == "edit")
{
EditIndex = e.Item.ItemIndex;
}
else if(e.CommandName == "save")
{
HtmlInputHidden t = e.Item.FindControl("firstNameHidden") as HtmlInputHidden;
Data.Contacts[e.Item.ItemIndex].FirstName = t.Value;
t = e.Item.FindControl("lastNameHidden") as HtmlInputHidden;
Data.Contacts[e.Item.ItemIndex].LastName = t.Value;
EditIndex = -1;
}
Repeater1.DataSource = Data.Contacts;
Repeater1.DataBind();
}
The Delete action is very simple. Just remove the item from the datasource based on the index. For editing, I set a ViewState backed property, EditIndex, to the index of the item wishing to be edited which will be used when the control is databound.
Although the TextBox controls could have been added in each row of the ItemTemplate and the visibility property used creates too much overhead. Editing is only done on one item at a time so to have many TextBoxes rendered, even thought they are invisible and may never be used is too heavy an approach. Instead, the controls will be added dynamically to the row being edited using a PlaceHolder control.
protected void OnItemDataBound(object sender, RepeaterItemEventArgs e)
{
if(e.Item.ItemType == ListItemType.Item ||
e.Item.ItemType == ListItemType.AlternatingItem)
{
if(e.Item.ItemIndex == EditIndex)
{
PlaceHolder p = e.Item.FindControl("firstNameEditPlaceholder") as PlaceHolder;
TextBox t = new TextBox();
t.ID = "firstNameEdit";
t.Text = ((Contact)e.Item.DataItem).FirstName;
p.Controls.Add(t);
Label l = e.Item.FindControl("firstName") as Label;
l.Visible = false;
p = e.Item.FindControl("lastNameEditPlaceholder") as PlaceHolder;
t = new TextBox();
t.ID = "lastNameEdit";
t.Text = ((Contact)e.Item.DataItem).LastName;
p.Controls.Add(t);
l = e.Item.FindControl("lastName") as Label;
l.Visible = false;
HtmlInputHidden h = e.Item.FindControl("firstNameHidden") as HtmlInputHidden;
h.Visible = true;
h = e.Item.FindControl("lastNameHidden") as HtmlInputHidden;
h.Visible = true;
ImageButton b = e.Item.FindControl("Edit") as ImageButton;
b.Visible = false;
b = e.Item.FindControl("Delete") as ImageButton;
b.CommandName = "save";
b.OnClientClick = "OnSave(this)";
b.ImageUrl = "~/Images/base_floppydisk_32.png";
}
}
}
The PlaceHolder control is necessary to dynamically add the TextBox controls because, although you can retrieve the Label control and find the Parent control, table cell in this case, you can't add the Textbox because ASP.NET generates the element as a LiteralControl which doesn't support adding child Controls. After adding the Textbox and assigning the current value from the datasource, the Label control is hidden by setting visibility=false then the HtmlHiddenInput fields are set with visiblity=true so they will be rendered and usable. The final step is to remove the edit button and reuse the delete button for saving the item once editing is complete. The reason for the HtmlHiddenInput is explained below.
Saving the Edited Item
Saving the in-place edit is where it gets a bit more complicated using server-side processing. When the Save button is clicked, the first event to be fired is the ItemCommand. However, since the edit controls were added dynamically in the ItemDataBound event, and this event has not been fired yet, they are not available. Likewise, since the control has not been bound to any data yet, the Label controls are being reconstituted from ViewState so they can't be used for temporary storage. In place of this, the HtmlHiddenInput controls are used with JQuery based JavaScript to transfer the values from the edit controls to the hidden fields. This is the only client-side code in this example.
function OnSave(obj)nSave(obj)
{
var tr = $(obj).closest("tr");
var firstNameEdit = tr.find("[id*='firstNameEdit']").val();
tr.find("[id*='firstNameHidden']").val(firstNameEdit);
var lastNameEdit = tr.find("[id*='lastNameEdit']").val();
tr.find("[id*='lastNameHidden']").val(lastNameEdit);
}
Client-side with AJAX
In the previous example, the ASP.NET engine was handling a lot of the rendering and behind the scenes processing, with a client-side approach, however, it must be implemented by hand.
The first thing to start with is hooking up the button events when the document is loaded. The live JQuery method is used for the edit and delete buttons so any new rows that are added will automatically have the events implements.
$(document).ready(function ()
{tion ()
{
$("[id*='edit']").live('click',OnEdit);
$("[id*='delete']").live('click', OnDelete);
$("[id*='add']").click(OnAdd);
});
The next step is to implement the functionality to add new contacts.
function OnAdd()
{
var tr = $(this).closest("tr");
var firstName = tr.find("#newFirstName");
var lastName = tr.find("#newLastName");
newRow = NewRow(tr);
newRow.find("span[id='firstName']").text(firstName.val());
newRow.find("span[id='lastName']").text(lastName.val());
AddContact(firstName.val(), lastName.val())
firstName.val("");
lastName.val("");
}
Here the this variable, which represents the Add button that was clicked, is used to find the table row it belongs to. From this, the input elements are found, the text that was entered by the user is extracted. The trick now comes in inserting a new row into the table. The NewRow will attempt to clone an existing row if any exists, otherwise the HTML must be created. An alternative would be to create the row in the markup but hide it from display, then use it for cloning.
function NewRow(tr)NewRow(tr)
{
if(tr.siblings().length != 1)
{
var clone = tr.prev().clone();
tr.before(clone);
}
else
{
var newRow = "<tr id=''>" +
"<td>" +
"<image id='edit' src='Images/EditDocument.png' class='imgButton' />" +
"<image id='delete'
src='Images/Delete_black_32x32.png' class='imgButton'/>" +
"</td>" +
"<td>" +
"<span ID='firstName'></span>" +
"</td>" +
"<td>" +
"<span ID='lastName'></span>" +
"</td>" +
"</tr>";
tr.before(newRow);
}
return tr.prev();
}
After the new row has been inserted and the text entered by the user has been updated, the next step is to add the new contact to the datastore on the server. This is accomplished with an AJAX call to a WebMethod on the page.
function AddContact(firstName, lastName)
{
var data = '{'
+ "\"firstName\":\"" + firstName + "\","
+ "\"lastName\":\"" + lastName + "\""
+ '}';
$.ajax({
type: "POST",
url: "AjaxEdit.aspx/AddContact",
data: data,
contentType: "application/json",
dataType: "json",
error: OnAjaxError,
success: OnAddContactSuccess
});
}
function OnAddContactSuccess(data)
{
var result = eval('(' + data.d + ')');
newRow.attr("id", result);
newRow = null;
}
There is nothing very complex here, just package the data and send it via AJAX to the method. If the method completes successfully, the id of the newly added contact is then assigned to the id attribute of the newly added row so it will be available during and edit or delete operation.
Edit and Save
Edit functionality is handled similarly to the server-side approach; insert input controls inline and replace the edit and delete buttons.
function OnEdit()
{
var tr = $(this).closest("tr");
var firstName = tr.find("span[id='firstName']");
var lastName = tr.find("span[id='lastName']");
firstName.before("<input id='firstNameEdit'
type='text' value='" + firstName.text() + "'/>").hide();
lastName.before("<input id='lastNameEdit'
type='text' value='" + lastName.text() + "'/>").hide();
tr.find("[id*='delete']").hide();
tr.find("[id*='edit']").before("<img id='save' src='images/base_floppydisk_32.png' />")
.hide();
tr.find("[id*='save']").one('click', OnSave);
}
Saving is once again similar; extract the values, update the labels, restore the buttons and update the datasource on the server.
function OnSave()
{
var tr = $(this).closest("tr");
var firstName = tr.find("[id='firstNameEdit']");
var lastName = tr.find("[id='lastNameEdit']");
tr.find("span[id='firstName']").text(firstName.val()).show();
tr.find("span[id='lastName']").text(lastName.val()).show();
firstName.remove();
lastName.remove();
tr.find("[id*='delete']").show();
tr.find("[id*='edit']").show();
tr.find("[id*='save']").remove();
UpdateContact( tr.attr("id"), firstName.val(), lastName.val())
}
Conclusion
The example was not meant to be an extensive exploration of the capabilities of the ASP.NET Repeater control, but hopefully it has successfully demonstrated how it could be used for CRUD operations against a datasource.
History