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